luci-mod-network: Rework the (multi) mac for static leases
[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;
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 = true;
320 s.addremove = false;
321
322 s.tab('general', _('General Settings'));
323 s.tab('advanced', _('Advanced Settings'));
324 s.tab('leases', _('Static Leases'));
325 s.tab('files', _('Resolv and Hosts Files'));
326 s.tab('hosts', _('Hostnames'));
327 s.tab('ipsets', _('IP Sets'));
328 s.tab('relay', _('Relay'));
329 s.tab('srvhosts', _('SRV'));
330 s.tab('mxhosts', _('MX'));
331 s.tab('cnamehosts', _('CNAME'));
332 s.tab('pxe_tftp', _('PXE/TFTP Settings'));
333
334 s.taboption('general', form.Flag, 'domainneeded',
335 _('Domain required'),
336 _('Never forward DNS queries which lack dots or domain parts.') + '<br />' +
337 customi18n(_('Names not in {etc_hosts} are answered {not_found}.') )
338 );
339 s.taboption('general', form.Flag, 'authoritative',
340 _('Authoritative'),
341 _('This is the only DHCP server in the local network.'));
342
343 o = s.taboption('general', form.Value, 'local',
344 _('Resolve these locally'),
345 _('Never forward these matching domains or subdomains; resolve from DHCP or hosts files only.'));
346 o.placeholder = '/internal.example.com/private.example.com/example.org';
347
348 s.taboption('general', form.Value, 'domain',
349 _('Local domain'),
350 _('Local domain suffix appended to DHCP names and hosts file entries.'));
351
352 o = s.taboption('general', form.Flag, 'logqueries',
353 _('Log queries'),
354 _('Write received DNS queries to syslog.') + ' ' + _('Dump cache on SIGUSR1, include requesting IP.'));
355 o.optional = true;
356
357 o = s.taboption('general', form.DynamicList, 'server',
358 _('DNS forwardings'),
359 _('Forward specific domain queries to specific upstream servers.'));
360 o.optional = true;
361 o.placeholder = '/*.example.org/10.1.2.3';
362 o.validate = validateServerSpec;
363
364 o = s.taboption('general', form.DynamicList, 'address',
365 _('Addresses'),
366 _('Resolve specified FQDNs to an IP.') + '<br />' +
367 customi18n(_('Syntax: {code_syntax}.'),
368 {code_syntax: '<code>/fqdn[/fqdn…]/[ipaddr]</code>'}) + '<br />' +
369 customi18n(_('{example_nx} returns {nxdomain}.',
370 'hint: <code>/example.com/</code> returns <code>NXDOMAIN</code>.'),
371 {example_nx: '<code>/example.com/</code>', nxdomain: '<code>NXDOMAIN</code>'}) + '<br />' +
372 customi18n(_('{any_domain} matches any domain (and returns {nxdomain}).',
373 'hint: <code>/#/</code> matches any domain (and returns NXDOMAIN).'),
374 {any_domain:'<code>/#/</code>', nxdomain: '<code>NXDOMAIN</code>'}) + '<br />' +
375 customi18n(
376 _('{example_null} returns {null_addr} addresses ({null_ipv4}, {null_ipv6}) for {example_com} and its subdomains.',
377 'hint: <code>/example.com/#</code> returns NULL addresses (<code>0.0.0.0</code>, <code>::</code>) for example.com and its subdomains.'),
378 { example_null: '<code>/example.com/#</code>',
379 null_addr: '<code>NULL</code>',
380 null_ipv4: '<code>0.0.0.0</code>',
381 null_ipv6: '<code>::</code>',
382 example_com: '<code>example.com</code>',
383 }
384 )
385 );
386 o.optional = true;
387 o.placeholder = '/router.local/router.lan/192.168.0.1';
388
389 o = s.taboption('general', form.DynamicList, 'ipset',
390 _('IP sets'),
391 _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.'));
392 o.optional = true;
393 o.placeholder = '/example.org/ipset,ipset6';
394
395 o = s.taboption('general', form.Flag, 'rebind_protection',
396 _('Rebind protection'),
397 customi18n(_('Discard upstream responses containing {rfc_1918_link} addresses.') ) + '<br />' +
398 customi18n(_('Discard also upstream responses containing {rfc_4193_link}, Link-Local and private IPv4-Mapped {rfc_4291_link} IPv6 Addresses.') )
399 );
400 o.rmempty = false;
401
402 o = s.taboption('general', form.Flag, 'rebind_localhost',
403 _('Allow localhost'),
404 customi18n(
405 _('Exempt {loopback_slash_8_v4} and {localhost_v6} from rebinding checks, e.g. for <abbr title="Real-time Block List">RBL</abbr> services.')
406 )
407 );
408 o.depends('rebind_protection', '1');
409
410 o = s.taboption('general', form.DynamicList, 'rebind_domain',
411 _('Domain whitelist'),
412 customi18n(_('List of domains to allow {rfc_1918_link} responses for.') )
413 );
414 o.depends('rebind_protection', '1');
415 o.optional = true;
416 o.placeholder = 'ihost.netflix.com';
417 o.validate = validateAddressList;
418
419 o = s.taboption('general', form.Flag, 'localservice',
420 _('Local service only'),
421 _('Accept DNS queries only from hosts whose address is on a local subnet.'));
422 o.optional = false;
423 o.rmempty = false;
424
425 o = s.taboption('general', form.Flag, 'nonwildcard',
426 _('Non-wildcard'),
427 _('Bind only to configured interface addresses, instead of the wildcard address.'));
428 o.default = o.enabled;
429 o.optional = false;
430 o.rmempty = true;
431
432 o = s.taboption('general', widgets.NetworkSelect, 'interface',
433 _('Listen interfaces'),
434 _('Listen only on the specified interfaces, and loopback if not excluded explicitly.'));
435 o.multiple = true;
436 o.nocreate = true;
437
438 o = s.taboption('general', widgets.NetworkSelect, 'notinterface',
439 _('Exclude interfaces'),
440 _('Do not listen on the specified interfaces.'));
441 o.loopback = true;
442 o.multiple = true;
443 o.nocreate = true;
444
445 o = s.taboption('relay', form.SectionValue, '__relays__', form.TableSection, 'relay', null,
446 _('Relay DHCP requests elsewhere. OK: v4↔v4, v6↔v6. Not OK: v4↔v6, v6↔v4.')
447 + '<br />' + _('Note: you may also need a DHCP Proxy (currently unavailable) when specifying a non-standard Relay To port(<code>addr#port</code>).')
448 + '<br />' + _('You may add multiple unique Relay To on the same Listen addr.'));
449
450 ss = o.subsection;
451
452 ss.addremove = true;
453 ss.anonymous = true;
454 ss.sortable = true;
455 ss.rowcolors = true;
456 ss.nodescriptions = true;
457
458 so = ss.option(form.Value, 'local_addr', _('Relay from'));
459 so.rmempty = false;
460 so.datatype = 'ipaddr';
461
462 for (var family = 4; family <= 6; family += 2) {
463 for (var i = 0; i < networks.length; i++) {
464 if (networks[i].getName() != 'loopback') {
465 var addrs = (family == 6) ? networks[i].getIP6Addrs() : networks[i].getIPAddrs();
466 for (var j = 0; j < addrs.length; j++) {
467 var addr = addrs[j].split('/')[0];
468 so.value(addr, E([], [
469 addr, ' (',
470 widgets.NetworkSelect.prototype.renderIfaceBadge(networks[i]),
471 ')'
472 ]));
473 }
474 }
475 }
476 }
477
478 so = ss.option(form.Value, 'server_addr', _('Relay to address'));
479 so.rmempty = false;
480 so.optional = false;
481 so.placeholder = '192.168.10.1#535';
482
483 so.validate = function(section, value) {
484 var m = this.section.formvalue(section, 'local_addr'),
485 n = this.section.formvalue(section, 'server_addr'),
486 p;
487 if (n != null && n != '')
488 p = n.split('#');
489 if (p.length > 1 && !/^[0-9]+$/.test(p[1]))
490 return _('Expected port number.');
491 else
492 n = p[0];
493
494 if ((m == null || m == '') && (n == null || n == ''))
495 return _('Both "Relay from" and "Relay to address" must be specified.');
496
497 if ((validation.parseIPv6(m) && validation.parseIPv6(n)) ||
498 validation.parseIPv4(m) && validation.parseIPv4(n))
499 return true;
500 else
501 return _('Address families of "Relay from" and "Relay to address" must match.')
502 };
503
504 so = ss.option(widgets.NetworkSelect, 'interface', _('Only accept replies via'));
505 so.optional = true;
506 so.rmempty = false;
507 so.placeholder = 'lan';
508
509 s.taboption('files', form.Flag, 'readethers',
510 customi18n(_('Use {etc_ethers}') ),
511 customi18n(_('Read {etc_ethers} to configure the DHCP server.') )
512 );
513
514 s.taboption('files', form.Value, 'leasefile',
515 _('Lease file'),
516 _('File to store DHCP lease information.'));
517
518 o = s.taboption('files', form.Flag, 'noresolv',
519 _('Ignore resolv file'));
520 o.optional = true;
521
522 o = s.taboption('files', form.Value, 'resolvfile',
523 _('Resolv file'),
524 _('File with upstream resolvers.'));
525 o.depends('noresolv', '0');
526 o.placeholder = '/tmp/resolv.conf.d/resolv.conf.auto';
527 o.optional = true;
528
529 o = s.taboption('files', form.Flag, 'nohosts',
530 customi18n(_('Ignore {etc_hosts}') )
531 );
532 o.optional = true;
533
534 o = s.taboption('files', form.DynamicList, 'addnhosts',
535 _('Additional hosts files'));
536 o.optional = true;
537 o.placeholder = '/etc/dnsmasq.hosts';
538
539 o = s.taboption('advanced', form.Flag, 'quietdhcp',
540 _('Suppress logging'),
541 _('Suppress logging of the routine operation for the DHCP protocol.'));
542 o.optional = true;
543
544 o = s.taboption('advanced', form.Flag, 'sequential_ip',
545 _('Allocate IPs sequentially'),
546 _('Allocate IP addresses sequentially, starting from the lowest available address.'));
547 o.optional = true;
548
549 o = s.taboption('advanced', form.Flag, 'boguspriv',
550 _('Filter private'),
551 customi18n(
552 _('Reject reverse lookups to {rfc_6303_link} IP ranges ({reverse_arpa}) not in {etc_hosts}.') )
553 );
554 o.default = o.enabled;
555
556 s.taboption('advanced', form.Flag, 'filterwin2k',
557 _('Filter SRV/SOA service discovery'),
558 _('Filters SRV/SOA service discovery, to avoid triggering dial-on-demand links.') + '<br />' +
559 _('May prevent VoIP or other services from working.'));
560
561 o = s.taboption('advanced', form.Flag, 'filter_aaaa',
562 _('Filter IPv6 AAAA records'),
563 _('Remove IPv6 addresses from the results and only return IPv4 addresses.') + '<br />' +
564 _('Can be useful if ISP has IPv6 nameservers but does not provide IPv6 routing.'));
565 o.optional = true;
566
567 o = s.taboption('advanced', form.Flag, 'filter_a',
568 _('Filter IPv4 A records'),
569 _('Remove IPv4 addresses from the results and only return IPv6 addresses.'));
570 o.optional = true;
571
572 s.taboption('advanced', form.Flag, 'localise_queries',
573 _('Localise queries'),
574 customi18n(_('Limit response records (from {etc_hosts}) to those that fall within the subnet of the querying interface.') ) + '<br />' +
575 _('This prevents unreachable IPs in subnets not accessible to you.') + '<br />' +
576 _('Note: IPv4 only.'));
577
578 if (L.hasSystemFeature('dnsmasq', 'dnssec')) {
579 o = s.taboption('advanced', form.Flag, 'dnssec',
580 _('DNSSEC'),
581 _('Validate DNS replies and cache DNSSEC data, requires upstream to support DNSSEC.'));
582 o.optional = true;
583
584 o = s.taboption('advanced', form.Flag, 'dnsseccheckunsigned',
585 _('DNSSEC check unsigned'),
586 _('Verify unsigned domain responses really come from unsigned domains.'));
587 o.default = o.enabled;
588 o.optional = true;
589 }
590
591 s.taboption('advanced', form.Flag, 'expandhosts',
592 _('Expand hosts'),
593 _('Add local domain suffix to names served from hosts files.'));
594
595 s.taboption('advanced', form.Flag, 'nonegcache',
596 _('No negative cache'),
597 _('Do not cache negative replies, e.g. for non-existent domains.'));
598
599 o = s.taboption('advanced', form.Value, 'serversfile',
600 _('Additional servers file'),
601 customi18n(_('File listing upstream resolvers, optionally domain-specific, e.g. {servers_file_entry01}, {servers_file_entry02}.') )
602 );
603 o.placeholder = '/etc/dnsmasq.servers';
604
605 o = s.taboption('advanced', form.Flag, 'strictorder',
606 _('Strict order'),
607 _('Upstream resolvers will be queried in the order of the resolv file.'));
608 o.optional = true;
609
610 o = s.taboption('advanced', form.Flag, 'allservers',
611 _('All servers'),
612 _('Query all available upstream resolvers.'));
613 o.optional = true;
614
615 o = s.taboption('advanced', form.DynamicList, 'bogusnxdomain',
616 customi18n(_('IPs to override with {nxdomain}') ),
617 customi18n(_('Transform replies which contain the specified addresses or subnets into {nxdomain} responses.') )
618 );
619 o.optional = true;
620 o.placeholder = '64.94.110.11';
621
622 o = s.taboption('advanced', form.Value, 'port',
623 _('DNS server port'),
624 _('Listening port for inbound DNS queries.'));
625 o.optional = true;
626 o.datatype = 'port';
627 o.placeholder = 53;
628
629 o = s.taboption('advanced', form.Value, 'queryport',
630 _('DNS query port'),
631 _('Fixed source port for outbound DNS queries.'));
632 o.optional = true;
633 o.datatype = 'port';
634 o.placeholder = _('any');
635
636 o = s.taboption('advanced', form.Value, 'dhcpleasemax',
637 _('Max. DHCP leases'),
638 _('Maximum allowed number of active DHCP leases.'));
639 o.optional = true;
640 o.datatype = 'uinteger';
641 o.placeholder = _('unlimited');
642
643 o = s.taboption('advanced', form.Value, 'ednspacket_max',
644 _('Max. EDNS0 packet size'),
645 _('Maximum allowed size of EDNS0 UDP packets.'));
646 o.optional = true;
647 o.datatype = 'uinteger';
648 o.placeholder = 1280;
649
650 o = s.taboption('advanced', form.Value, 'dnsforwardmax',
651 _('Max. concurrent queries'),
652 _('Maximum allowed number of concurrent DNS queries.'));
653 o.optional = true;
654 o.datatype = 'uinteger';
655 o.placeholder = 150;
656
657 o = s.taboption('advanced', form.Value, 'cachesize',
658 _('Size of DNS query cache'),
659 _('Number of cached DNS entries, 10000 is maximum, 0 is no caching.'));
660 o.optional = true;
661 o.datatype = 'range(0,10000)';
662 o.placeholder = 1000;
663
664 o = s.taboption('pxe_tftp', form.Flag, 'enable_tftp',
665 _('Enable TFTP server'),
666 _('Enable the built-in single-instance TFTP server.'));
667 o.optional = true;
668
669 o = s.taboption('pxe_tftp', form.Value, 'tftp_root',
670 _('TFTP server root'),
671 _('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>.'));
672 o.depends('enable_tftp', '1');
673 o.optional = true;
674 o.placeholder = '/';
675
676 o = s.taboption('pxe_tftp', form.Value, 'dhcp_boot',
677 _('Network boot image'),
678 _('Filename of the boot image advertised to clients.'));
679 o.depends('enable_tftp', '1');
680 o.optional = true;
681 o.placeholder = 'pxelinux.0';
682
683 /* PXE - https://openwrt.org/docs/guide-user/base-system/dhcp#booting_options */
684 o = s.taboption('pxe_tftp', form.SectionValue, '__pxe__', form.GridSection, 'boot', null,
685 _('Special <abbr title="Preboot eXecution Environment">PXE</abbr> boot options for Dnsmasq.'));
686 ss = o.subsection;
687 ss.addremove = true;
688 ss.anonymous = true;
689 ss.nodescriptions = true;
690
691 so = ss.option(form.Value, 'filename',
692 _('Filename'),
693 _('Host requests this filename from the boot server.'));
694 so.optional = false;
695 so.placeholder = 'pxelinux.0';
696
697 so = ss.option(form.Value, 'servername',
698 _('Server name'),
699 _('The hostname of the boot server'));
700 so.optional = false;
701 so.placeholder = 'myNAS';
702
703 so = ss.option(form.Value, 'serveraddress',
704 _('Server address'),
705 _('The IP address of the boot server'));
706 so.optional = false;
707 so.placeholder = '192.168.1.2';
708
709 so = ss.option(form.DynamicList, 'dhcp_option',
710 _('DHCP Options'),
711 _('Options for the Network-ID. (Note: needs also Network-ID.) E.g. "<code>42,192.168.1.4</code>" for NTP server, "<code>3,192.168.4.4</code>" for default route. <code>0.0.0.0</code> means "the address of the system running dnsmasq".'));
712 so.optional = true;
713 so.placeholder = '42,192.168.1.4';
714
715 so = ss.option(widgets.DeviceSelect, 'networkid',
716 _('Network-ID'),
717 _('Apply DHCP Options to this net. (Empty = all clients).'));
718 so.optional = true;
719 so.noaliases = true;
720
721 so = ss.option(form.Flag, 'force',
722 _('Force'),
723 _('Always send DHCP Options. Sometimes needed, with e.g. PXELinux.'));
724 so.optional = true;
725
726 so = ss.option(form.Value, 'instance',
727 _('Instance'),
728 _('Dnsmasq instance to which this boot section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
729 so.optional = true;
730
731 Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
732 so.value(generateDnsmasqInstanceEntry(val));
733 });
734
735 o = s.taboption('srvhosts', form.SectionValue, '__srvhosts__', form.TableSection, 'srvhost', null,
736 _('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')
737 + '<br />' + _('_service: _sip, _ldap, _imap, _stun, _xmpp-client, … . (Note: while _http is possible, no browsers support SRV records.)')
738 + '<br />' + _('_proto: _tcp, _udp, _sctp, _quic, … .')
739 + '<br />' + _('You may add multiple records for the same Target.')
740 + '<br />' + _('Larger weights (of the same prio) are given a proportionately higher probability of being selected.'));
741
742 ss = o.subsection;
743
744 ss.addremove = true;
745 ss.anonymous = true;
746 ss.sortable = true;
747 ss.rowcolors = true;
748
749 so = ss.option(form.Value, 'srv', _('SRV'), _('Syntax:') + ' ' + '<code>_service._proto.example.com.</code>');
750 so.rmempty = false;
751 so.datatype = 'hostname';
752 so.placeholder = '_sip._tcp.example.com.';
753
754 so = ss.option(form.Value, 'target', _('Target'), _('CNAME or fqdn'));
755 so.rmempty = false;
756 so.datatype = 'hostname';
757 so.placeholder = 'sip.example.com.';
758
759 so = ss.option(form.Value, 'port', _('Port'));
760 so.rmempty = false;
761 so.datatype = 'port';
762 so.placeholder = '5060';
763
764 so = ss.option(form.Value, 'class', _('Priority'), _('Ordinal: lower comes first.'));
765 so.rmempty = true;
766 so.datatype = 'range(0,65535)';
767 so.placeholder = '10';
768
769 so = ss.option(form.Value, 'weight', _('Weight'));
770 so.rmempty = true;
771 so.datatype = 'range(0,65535)';
772 so.placeholder = '50';
773
774 o = s.taboption('mxhosts', form.SectionValue, '__mxhosts__', form.TableSection, 'mxhost', null,
775 _('Bind service records to a domain name: specify the location of services.')
776 + '<br />' + _('You may add multiple records for the same domain.'));
777
778 ss = o.subsection;
779
780 ss.addremove = true;
781 ss.anonymous = true;
782 ss.sortable = true;
783 ss.rowcolors = true;
784 ss.nodescriptions = true;
785
786 so = ss.option(form.Value, 'domain', _('Domain'));
787 so.rmempty = false;
788 so.datatype = 'hostname';
789 so.placeholder = 'example.com.';
790
791 so = ss.option(form.Value, 'relay', _('Relay'));
792 so.rmempty = false;
793 so.datatype = 'hostname';
794 so.placeholder = 'relay.example.com.';
795
796 so = ss.option(form.Value, 'pref', _('Priority'), _('Ordinal: lower comes first.'));
797 so.rmempty = true;
798 so.datatype = 'range(0,65535)';
799 so.placeholder = '0';
800
801 o = s.taboption('cnamehosts', form.SectionValue, '__cname__', form.TableSection, 'cname', null,
802 _('Set an alias for a hostname.'));
803
804 ss = o.subsection;
805
806 ss.addremove = true;
807 ss.anonymous = true;
808 ss.sortable = true;
809 ss.rowcolors = true;
810 ss.nodescriptions = true;
811
812 so = ss.option(form.Value, 'cname', _('Domain'));
813 so.rmempty = false;
814 so.datatype = 'hostname';
815 so.placeholder = 'www.example.com.';
816
817 so = ss.option(form.Value, 'target', _('Target'));
818 so.rmempty = false;
819 so.datatype = 'hostname';
820 so.placeholder = 'example.com.';
821
822 o = s.taboption('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
823 _('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.'));
824
825 ss = o.subsection;
826
827 ss.addremove = true;
828 ss.anonymous = true;
829 ss.sortable = true;
830
831 so = ss.option(form.Value, 'name', _('Hostname'));
832 so.rmempty = false;
833 so.datatype = 'hostname';
834
835 so = ss.option(form.Value, 'ip', _('IP address'));
836 so.rmempty = false;
837 so.datatype = 'ipaddr';
838
839 var ipaddrs = {};
840
841 Object.keys(hosts).forEach(function(mac) {
842 var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4);
843
844 for (var i = 0; i < addrs.length; i++)
845 ipaddrs[addrs[i]] = hosts[mac].name || mac;
846 });
847
848 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
849 so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
850 });
851
852 o = s.taboption('ipsets', form.SectionValue, '__ipsets__', form.GridSection, 'ipset', null,
853 _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.') + '<br />' +
854 _('The netfilter components below are only regarded when running fw4.'));
855
856 ss = o.subsection;
857
858 ss.addremove = true;
859 ss.anonymous = true;
860 ss.sortable = true;
861 ss.rowcolors = true;
862 ss.nodescriptions = true;
863 ss.modaltitle = _('Edit IP set');
864
865 so = ss.option(form.DynamicList, 'name', _('Name of the set'));
866 so.rmempty = false;
867 so.editable = true;
868 so.datatype = 'string';
869
870 so = ss.option(form.DynamicList, 'domain', _('FQDN'));
871 so.rmempty = false;
872 so.editable = true;
873 so.datatype = 'hostname';
874
875 so = ss.option(form.Value, 'table', _('Netfilter table name'), _('Defaults to fw4.'));
876 so.editable = true;
877 so.placeholder = 'fw4';
878 so.rmempty = true;
879
880 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 />' +
881 _('Adding an IPv6 to an IPv4 set and vice-versa silently fails.'));
882 so.editable = true;
883 so.rmempty = true;
884 so.value('inet', _('IPv4+6'));
885 so.value('ip', _('IPv4'));
886 so.value('ip6', _('IPv6'));
887
888 o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
889 _('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 />' +
890 _('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 />' +
891 _('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).'));
892
893 ss = o.subsection;
894
895 ss.addremove = true;
896 ss.anonymous = true;
897 ss.sortable = true;
898 ss.nodescriptions = true;
899 ss.max_cols = 8;
900 ss.modaltitle = _('Edit static lease');
901
902 so = ss.option(form.Value, 'name',
903 _('Hostname'),
904 _('Optional hostname to assign'));
905 so.validate = validateHostname;
906 so.rmempty = true;
907 so.write = function(section, value) {
908 uci.set('dhcp', section, 'name', value);
909 uci.set('dhcp', section, 'dns', '1');
910 };
911 so.remove = function(section) {
912 uci.unset('dhcp', section, 'name');
913 uci.unset('dhcp', section, 'dns');
914 };
915
916 //this can be a .DynamicList or a .Value with a widget and dnsmasq handles multimac OK.
917 so = ss.option(form.DynamicList, 'mac',
918 _('MAC address(es)'),
919 _('The hardware address(es) of this entry/host.') + '<br /><br />' +
920 _('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.'));
921 //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
922 so.rmempty = true;
923 so.cfgvalue = function(section) {
924 var macs = L.toArray(uci.get('dhcp', section, 'mac'));
925 return expandAndFormatMAC(macs);
926 };
927 //removed jows renderwidget function which hindered multi-mac entry
928 so.validate = validateMACAddr.bind(so, pools);
929 Object.keys(hosts).forEach(function(mac) {
930 var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
931 so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
932 });
933
934 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.'));
935 so.value('ignore', _('Ignore'));
936 so.datatype = 'or(ip4addr,"ignore")';
937 so.validate = function(section, value) {
938 var m = this.section.formvalue(section, 'mac'),
939 n = this.section.formvalue(section, 'name');
940
941 if ((m == null || m == '') && (n == null || n == ''))
942 return _('One of hostname or MAC address must be specified!');
943
944 if (value == null || value == '' || value == 'ignore')
945 return true;
946
947 var leases = uci.sections('dhcp', 'host');
948
949 for (var i = 0; i < leases.length; i++)
950 if (leases[i]['.name'] != section && leases[i].ip == value)
951 return _('The IP address %h is already used by another static lease').format(value);
952
953 for (var i = 0; i < pools.length; i++) {
954 var net_mask = calculateNetwork(value, pools[i].netmask);
955
956 if (net_mask && net_mask[0] == pools[i].network)
957 return true;
958 }
959
960 return _('The IP address is outside of any DHCP pool address range');
961 };
962
963 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
964 so.value(ipv4, ipaddrs[ipv4] ? '%s (%s)'.format(ipv4, ipaddrs[ipv4]) : ipv4);
965 });
966
967 so = ss.option(form.Value, 'leasetime',
968 _('Lease time'),
969 _('Host-specific lease time, e.g. <code>5m</code>, <code>3h</code>, <code>7d</code>.'));
970 so.rmempty = true;
971 so.value('5m', _('5m (5 minutes)'));
972 so.value('3h', _('3h (3 hours)'));
973 so.value('12h', _('12h (12 hours - default)'));
974 so.value('7d', _('7d (7 days)'));
975 so.value('infinite', _('infinite (lease does not expire)'));
976
977 so = ss.option(form.Value, 'duid',
978 _('DUID'),
979 _('The DHCPv6-DUID (DHCP unique identifier) of this host.'));
980 so.datatype = 'and(rangelength(20,36),hexstring)';
981 Object.keys(duids).forEach(function(duid) {
982 so.value(duid, '%s (%s)'.format(duid, duids[duid].hostname || duids[duid].macaddr || duids[duid].ip6addr || '?'));
983 });
984
985 so = ss.option(form.Value, 'hostid',
986 _('IPv6-Suffix (hex)'),
987 _('The IPv6 interface identifier (address suffix) as hexadecimal number (max. 16 chars).'));
988 so.datatype = 'and(rangelength(0,16),hexstring)';
989
990 so = ss.option(form.DynamicList, 'tag',
991 _('Tag'),
992 _('Assign new, freeform tags to this entry.'));
993
994 so = ss.option(form.DynamicList, 'match_tag',
995 _('Match Tag'),
996 _('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 />' +
997 _('Ignore requests from unknown machines using %s.').format('<code>!known</code>') + '<br /><br />' +
998 _('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>'));
999 so.value('known', _('known'));
1000 so.value('!known', _('!known (not known)'));
1001 so.value('known-othernet', _('known-othernet (on different subnet)'));
1002 so.optional = true;
1003
1004 so = ss.option(form.Value, 'instance',
1005 _('Instance'),
1006 _('Dnsmasq instance to which this DHCP host section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
1007 so.optional = true;
1008
1009 Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
1010 so.value(generateDnsmasqInstanceEntry(val));
1011 });
1012
1013
1014 so = ss.option(form.Flag, 'broadcast',
1015 _('Broadcast'),
1016 _('Force broadcast DHCP response.'));
1017
1018 so = ss.option(form.Flag, 'dns',
1019 _('Forward/reverse DNS'),
1020 _('Add static forward and reverse DNS entries for this host.'));
1021
1022 o = s.taboption('leases', CBILeaseStatus, '__status__');
1023
1024 if (has_dhcpv6)
1025 o = s.taboption('leases', CBILease6Status, '__status6__');
1026
1027 return m.render().then(function(mapEl) {
1028 poll.add(function() {
1029 return callDHCPLeases().then(function(leaseinfo) {
1030 var leases = Array.isArray(leaseinfo.dhcp_leases) ? leaseinfo.dhcp_leases : [],
1031 leases6 = Array.isArray(leaseinfo.dhcp6_leases) ? leaseinfo.dhcp6_leases : [];
1032
1033 cbi_update_table(mapEl.querySelector('#lease_status_table'),
1034 leases.map(function(lease) {
1035 var exp;
1036
1037 if (lease.expires === false)
1038 exp = E('em', _('unlimited'));
1039 else if (lease.expires <= 0)
1040 exp = E('em', _('expired'));
1041 else
1042 exp = '%t'.format(lease.expires);
1043
1044 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
1045 name = hint ? hint.name : null,
1046 host = null;
1047
1048 if (name && lease.hostname && lease.hostname != name)
1049 host = '%s (%s)'.format(lease.hostname, name);
1050 else if (lease.hostname)
1051 host = lease.hostname;
1052
1053 return [
1054 host || '-',
1055 lease.ipaddr,
1056 lease.macaddr,
1057 exp
1058 ];
1059 }),
1060 E('em', _('There are no active leases')));
1061
1062 if (has_dhcpv6) {
1063 cbi_update_table(mapEl.querySelector('#lease6_status_table'),
1064 leases6.map(function(lease) {
1065 var exp;
1066
1067 if (lease.expires === false)
1068 exp = E('em', _('unlimited'));
1069 else if (lease.expires <= 0)
1070 exp = E('em', _('expired'));
1071 else
1072 exp = '%t'.format(lease.expires);
1073
1074 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
1075 name = hint ? (hint.name || L.toArray(hint.ipaddrs || hint.ipv4)[0] || L.toArray(hint.ip6addrs || hint.ipv6)[0]) : null,
1076 host = null;
1077
1078 if (name && lease.hostname && lease.hostname != name && lease.ip6addr != name)
1079 host = '%s (%s)'.format(lease.hostname, name);
1080 else if (lease.hostname)
1081 host = lease.hostname;
1082 else if (name)
1083 host = name;
1084
1085 return [
1086 host || '-',
1087 lease.ip6addrs ? lease.ip6addrs.join(' ') : lease.ip6addr,
1088 lease.duid,
1089 exp
1090 ];
1091 }),
1092 E('em', _('There are no active leases')));
1093 }
1094 });
1095 });
1096
1097 return mapEl;
1098 });
1099 }
1100 });