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