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