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