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