Merge pull request #6335 from arbolitoloco1/patch-4
[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.getDevices()
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 ndevs = 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('relay', _('Relay'));
258 s.tab('files', _('Resolv and Hosts Files'));
259 s.tab('pxe_tftp', _('PXE/TFTP Settings'));
260 s.tab('advanced', _('Advanced Settings'));
261 s.tab('leases', _('Static Leases'));
262 s.tab('hosts', _('Hostnames'));
263 s.tab('srvhosts', _('SRV'));
264 s.tab('mxhosts', _('MX'));
265 s.tab('cnamehosts', _('CNAME'));
266 s.tab('ipsets', _('IP Sets'));
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, 'id', _('ID'));
368 so.rmempty = false;
369 so.optional = true;
370
371 so = ss.option(widgets.NetworkSelect, 'interface', _('Interface'));
372 so.optional = true;
373 so.rmempty = false;
374 so.placeholder = 'lan';
375
376 so = ss.option(form.Value, 'local_addr', _('Listen address'));
377 so.rmempty = false;
378 so.datatype = 'ipaddr';
379
380 for (var family = 4; family <= 6; family += 2) {
381 for (var i = 0; i < ndevs.length; i++) {
382 var addrs = (family == 6) ? ndevs[i].getIP6Addrs() : ndevs[i].getIPAddrs();
383 for (var j = 0; j < addrs.length; j++)
384 so.value(addrs[j].split('/')[0]);
385 }
386 }
387
388 so = ss.option(form.Value, 'server_addr', _('Relay To address'));
389 so.rmempty = false;
390 so.optional = false;
391 so.placeholder = '192.168.10.1#535';
392
393 so.validate = function(section, value) {
394 var m = this.section.formvalue(section, 'local_addr'),
395 n = this.section.formvalue(section, 'server_addr'),
396 p;
397 if (n != null && n != '')
398 p = n.split('#');
399 if (p.length > 1 && !/^[0-9]+$/.test(p[1]))
400 return _('Expected port number.');
401 else
402 n = p[0];
403
404 if ((m == null || m == '') && (n == null || n == ''))
405 return _('Both Listen addr and Relay To must be specified.');
406
407 if ((validation.parseIPv6(m) && validation.parseIPv6(n)) ||
408 validation.parseIPv4(m) && validation.parseIPv4(n))
409 return true;
410 else
411 return _('Listen and Relay To IP family must be homogeneous.')
412 };
413
414 s.taboption('files', form.Flag, 'readethers',
415 _('Use <code>/etc/ethers</code>'),
416 _('Read <code>/etc/ethers</code> to configure the DHCP server.'));
417
418 s.taboption('files', form.Value, 'leasefile',
419 _('Lease file'),
420 _('File to store DHCP lease information.'));
421
422 o = s.taboption('files', form.Flag, 'noresolv',
423 _('Ignore resolv file'));
424 o.optional = true;
425
426 o = s.taboption('files', form.Value, 'resolvfile',
427 _('Resolv file'),
428 _('File with upstream resolvers.'));
429 o.depends('noresolv', '0');
430 o.placeholder = '/tmp/resolv.conf.d/resolv.conf.auto';
431 o.optional = true;
432
433 o = s.taboption('files', form.Flag, 'nohosts',
434 _('Ignore <code>/etc/hosts</code>'));
435 o.optional = true;
436
437 o = s.taboption('files', form.DynamicList, 'addnhosts',
438 _('Additional hosts files'));
439 o.optional = true;
440 o.placeholder = '/etc/dnsmasq.hosts';
441
442 o = s.taboption('advanced', form.Flag, 'quietdhcp',
443 _('Suppress logging'),
444 _('Suppress logging of the routine operation for the DHCP protocol.'));
445 o.optional = true;
446
447 o = s.taboption('advanced', form.Flag, 'sequential_ip',
448 _('Allocate IPs sequentially'),
449 _('Allocate IP addresses sequentially, starting from the lowest available address.'));
450 o.optional = true;
451
452 o = s.taboption('advanced', form.Flag, 'boguspriv',
453 _('Filter private'),
454 _('Do not forward reverse lookups for local networks.'));
455 o.default = o.enabled;
456
457 s.taboption('advanced', form.Flag, 'filterwin2k',
458 _('Filter SRV/SOA service discovery'),
459 _('Filters SRV/SOA service discovery, to avoid triggering dial-on-demand links.') + '<br />' +
460 _('May prevent VoIP or other services from working.'));
461
462 o = s.taboption('advanced', form.Flag, 'filter_aaaa',
463 _('Filter IPv6 AAAA records'),
464 _('Remove IPv6 addresses from the results and only return IPv4 addresses.') + '<br />' +
465 _('Can be useful if ISP has IPv6 nameservers but does not provide IPv6 routing.'));
466 o.optional = true;
467
468 o = s.taboption('advanced', form.Flag, 'filter_a',
469 _('Filter IPv4 A records'),
470 _('Remove IPv4 addresses from the results and only return IPv6 addresses.'));
471 o.optional = true;
472
473 s.taboption('advanced', form.Flag, 'localise_queries',
474 _('Localise queries'),
475 _('Return answers to DNS queries matching the subnet from which the query was received if multiple IPs are available.'));
476
477 if (L.hasSystemFeature('dnsmasq', 'dnssec')) {
478 o = s.taboption('advanced', form.Flag, 'dnssec',
479 _('DNSSEC'),
480 _('Validate DNS replies and cache DNSSEC data, requires upstream to support DNSSEC.'));
481 o.optional = true;
482
483 o = s.taboption('advanced', form.Flag, 'dnsseccheckunsigned',
484 _('DNSSEC check unsigned'),
485 _('Verify unsigned domain responses really come from unsigned domains.'));
486 o.default = o.enabled;
487 o.optional = true;
488 }
489
490 s.taboption('advanced', form.Flag, 'expandhosts',
491 _('Expand hosts'),
492 _('Add local domain suffix to names served from hosts files.'));
493
494 s.taboption('advanced', form.Flag, 'nonegcache',
495 _('No negative cache'),
496 _('Do not cache negative replies, e.g. for non-existent domains.'));
497
498 o = s.taboption('advanced', form.Value, 'serversfile',
499 _('Additional servers file'),
500 _('File listing upstream resolvers, optionally domain-specific, e.g. <code>server=1.2.3.4</code>, <code>server=/domain/1.2.3.4</code>.'));
501 o.placeholder = '/etc/dnsmasq.servers';
502
503 o = s.taboption('advanced', form.Flag, 'strictorder',
504 _('Strict order'),
505 _('Upstream resolvers will be queried in the order of the resolv file.'));
506 o.optional = true;
507
508 o = s.taboption('advanced', form.Flag, 'allservers',
509 _('All servers'),
510 _('Query all available upstream resolvers.'));
511 o.optional = true;
512
513 o = s.taboption('advanced', form.DynamicList, 'bogusnxdomain',
514 _('IPs to override with NXDOMAIN'),
515 _('List of IP addresses to convert into NXDOMAIN responses.'));
516 o.optional = true;
517 o.placeholder = '64.94.110.11';
518
519 o = s.taboption('advanced', form.Value, 'port',
520 _('DNS server port'),
521 _('Listening port for inbound DNS queries.'));
522 o.optional = true;
523 o.datatype = 'port';
524 o.placeholder = 53;
525
526 o = s.taboption('advanced', form.Value, 'queryport',
527 _('DNS query port'),
528 _('Fixed source port for outbound DNS queries.'));
529 o.optional = true;
530 o.datatype = 'port';
531 o.placeholder = _('any');
532
533 o = s.taboption('advanced', form.Value, 'dhcpleasemax',
534 _('Max. DHCP leases'),
535 _('Maximum allowed number of active DHCP leases.'));
536 o.optional = true;
537 o.datatype = 'uinteger';
538 o.placeholder = _('unlimited');
539
540 o = s.taboption('advanced', form.Value, 'ednspacket_max',
541 _('Max. EDNS0 packet size'),
542 _('Maximum allowed size of EDNS0 UDP packets.'));
543 o.optional = true;
544 o.datatype = 'uinteger';
545 o.placeholder = 1280;
546
547 o = s.taboption('advanced', form.Value, 'dnsforwardmax',
548 _('Max. concurrent queries'),
549 _('Maximum allowed number of concurrent DNS queries.'));
550 o.optional = true;
551 o.datatype = 'uinteger';
552 o.placeholder = 150;
553
554 o = s.taboption('advanced', form.Value, 'cachesize',
555 _('Size of DNS query cache'),
556 _('Number of cached DNS entries, 10000 is maximum, 0 is no caching.'));
557 o.optional = true;
558 o.datatype = 'range(0,10000)';
559 o.placeholder = 1000;
560
561 o = s.taboption('pxe_tftp', form.Flag, 'enable_tftp',
562 _('Enable TFTP server'),
563 _('Enable the built-in single-instance TFTP server.'));
564 o.optional = true;
565
566 o = s.taboption('pxe_tftp', form.Value, 'tftp_root',
567 _('TFTP server root'),
568 _('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>.'));
569 o.depends('enable_tftp', '1');
570 o.optional = true;
571 o.placeholder = '/';
572
573 o = s.taboption('pxe_tftp', form.Value, 'dhcp_boot',
574 _('Network boot image'),
575 _('Filename of the boot image advertised to clients.'));
576 o.depends('enable_tftp', '1');
577 o.optional = true;
578 o.placeholder = 'pxelinux.0';
579
580 /* PXE - https://openwrt.org/docs/guide-user/base-system/dhcp#booting_options */
581 o = s.taboption('pxe_tftp', form.SectionValue, '__pxe__', form.GridSection, 'boot', null,
582 _('Special <abbr title="Preboot eXecution Environment">PXE</abbr> boot options for Dnsmasq.'));
583 ss = o.subsection;
584 ss.addremove = true;
585 ss.anonymous = true;
586 ss.nodescriptions = true;
587
588 so = ss.option(form.Value, 'filename',
589 _('Filename'),
590 _('Host requests this filename from the boot server.'));
591 so.optional = false;
592 so.placeholder = 'pxelinux.0';
593
594 so = ss.option(form.Value, 'servername',
595 _('Server name'),
596 _('The hostname of the boot server'));
597 so.optional = false;
598 so.placeholder = 'myNAS';
599
600 so = ss.option(form.Value, 'serveraddress',
601 _('Server address'),
602 _('The IP address of the boot server'));
603 so.optional = false;
604 so.placeholder = '192.168.1.2';
605
606 so = ss.option(form.DynamicList, 'dhcp_option',
607 _('DHCP Options'),
608 _('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".'));
609 so.optional = true;
610 so.placeholder = '42,192.168.1.4';
611
612 so = ss.option(widgets.DeviceSelect, 'networkid',
613 _('Network-ID'),
614 _('Apply DHCP Options to this net. (Empty = all clients).'));
615 so.optional = true;
616 so.noaliases = true;
617
618 so = ss.option(form.Flag, 'force',
619 _('Force'),
620 _('Always send DHCP Options. Sometimes needed, with e.g. PXELinux.'));
621 so.optional = true;
622
623 so = ss.option(form.Value, 'instance',
624 _('Instance'),
625 _('Dnsmasq instance to which this boot section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
626 so.optional = true;
627
628 Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
629 so.value(index, '%s (Domain: %s, Local: %s)'.format(index, val.domain || '?', val.local || '?'));
630 });
631
632 o = s.taboption('srvhosts', form.SectionValue, '__srvhosts__', form.TableSection, 'srvhost', null,
633 _('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')
634 + '<br />' + _('_service: _sip, _ldap, _imap, _stun, _xmpp-client, … . (Note: while _http is possible, no browsers support SRV records.)')
635 + '<br />' + _('_proto: _tcp, _udp, _sctp, _quic, … .')
636 + '<br />' + _('You may add multiple records for the same Target.')
637 + '<br />' + _('Larger weights (of the same prio) are given a proportionately higher probability of being selected.'));
638
639 ss = o.subsection;
640
641 ss.addremove = true;
642 ss.anonymous = true;
643 ss.sortable = true;
644 ss.rowcolors = true;
645
646 so = ss.option(form.Value, 'srv', _('SRV'), _('Syntax: <code>_service._proto.example.com</code>.'));
647 so.rmempty = false;
648 so.datatype = 'hostname';
649 so.placeholder = '_sip._tcp.example.com';
650
651 so = ss.option(form.Value, 'target', _('Target'), _('CNAME or fqdn'));
652 so.rmempty = false;
653 so.datatype = 'hostname';
654 so.placeholder = 'sip.example.com';
655
656 so = ss.option(form.Value, 'port', _('Port'));
657 so.rmempty = false;
658 so.datatype = 'port';
659 so.placeholder = '5060';
660
661 so = ss.option(form.Value, 'class', _('Priority'), _('Ordinal: lower comes first.'));
662 so.rmempty = true;
663 so.datatype = 'range(0,65535)';
664 so.placeholder = '10';
665
666 so = ss.option(form.Value, 'weight', _('Weight'));
667 so.rmempty = true;
668 so.datatype = 'range(0,65535)';
669 so.placeholder = '50';
670
671 o = s.taboption('mxhosts', form.SectionValue, '__mxhosts__', form.TableSection, 'mxhost', null,
672 _('Bind service records to a domain name: specify the location of services.')
673 + '<br />' + _('You may add multiple records for the same domain.'));
674
675 ss = o.subsection;
676
677 ss.addremove = true;
678 ss.anonymous = true;
679 ss.sortable = true;
680 ss.rowcolors = true;
681 ss.nodescriptions = true;
682
683 so = ss.option(form.Value, 'domain', _('Domain'));
684 so.rmempty = false;
685 so.datatype = 'hostname';
686 so.placeholder = 'example.com';
687
688 so = ss.option(form.Value, 'relay', _('Relay'));
689 so.rmempty = false;
690 so.datatype = 'hostname';
691 so.placeholder = 'relay.example.com';
692
693 so = ss.option(form.Value, 'pref', _('Priority'), _('Ordinal: lower comes first.'));
694 so.rmempty = true;
695 so.datatype = 'range(0,65535)';
696 so.placeholder = '0';
697
698 o = s.taboption('cnamehosts', form.SectionValue, '__cname__', form.TableSection, 'cname', null,
699 _('Set an alias for a hostname.'));
700
701 ss = o.subsection;
702
703 ss.addremove = true;
704 ss.anonymous = true;
705 ss.sortable = true;
706 ss.rowcolors = true;
707 ss.nodescriptions = true;
708
709 so = ss.option(form.Value, 'cname', _('Domain'));
710 so.rmempty = false;
711 so.datatype = 'hostname';
712 so.placeholder = 'www.example.com';
713
714 so = ss.option(form.Value, 'target', _('Target'));
715 so.rmempty = false;
716 so.datatype = 'hostname';
717 so.placeholder = 'example.com';
718
719 o = s.taboption('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
720 _('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.'));
721
722 ss = o.subsection;
723
724 ss.addremove = true;
725 ss.anonymous = true;
726 ss.sortable = true;
727
728 so = ss.option(form.Value, 'name', _('Hostname'));
729 so.rmempty = false;
730 so.datatype = 'hostname';
731
732 so = ss.option(form.Value, 'ip', _('IP address'));
733 so.rmempty = false;
734 so.datatype = 'ipaddr';
735
736 var ipaddrs = {};
737
738 Object.keys(hosts).forEach(function(mac) {
739 var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4);
740
741 for (var i = 0; i < addrs.length; i++)
742 ipaddrs[addrs[i]] = hosts[mac].name || mac;
743 });
744
745 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
746 so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
747 });
748
749 o = s.taboption('ipsets', form.SectionValue, '__ipsets__', form.GridSection, 'ipset', null,
750 _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.'));
751
752 ss = o.subsection;
753
754 ss.addremove = true;
755 ss.anonymous = true;
756 ss.sortable = true;
757
758 so = ss.option(form.DynamicList, 'name', _('IP set'));
759 so.rmempty = false;
760 so.datatype = 'string';
761
762 so = ss.option(form.DynamicList, 'domain', _('Domain'));
763 so.rmempty = false;
764 so.datatype = 'hostname';
765
766 o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
767 _('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 />' +
768 _('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 />' +
769 _('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).'));
770
771 ss = o.subsection;
772
773 ss.addremove = true;
774 ss.anonymous = true;
775 ss.sortable = true;
776 ss.nodescriptions = true;
777 ss.max_cols = 8;
778 ss.modaltitle = _('Edit static lease');
779
780 so = ss.option(form.Value, 'name',
781 _('Hostname'),
782 _('Optional hostname to assign'));
783 so.validate = validateHostname;
784 so.rmempty = true;
785 so.write = function(section, value) {
786 uci.set('dhcp', section, 'name', value);
787 uci.set('dhcp', section, 'dns', '1');
788 };
789 so.remove = function(section) {
790 uci.unset('dhcp', section, 'name');
791 uci.unset('dhcp', section, 'dns');
792 };
793
794 so = ss.option(form.Value, 'mac',
795 _('MAC address(es)'),
796 _('The hardware address(es) of this entry/host, separated by spaces.') + '<br /><br />' +
797 _('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.'));
798 //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
799 so.validate = function(section_id, value) {
800 var macaddrs = L.toArray(value);
801
802 for (var i = 0; i < macaddrs.length; i++)
803 if (!macaddrs[i].match(/^([a-fA-F0-9]{2}|\*):([a-fA-F0-9]{2}:|\*:){4}(?:[a-fA-F0-9]{2}|\*)$/))
804 return _('Expecting a valid MAC address, optionally including wildcards');
805
806 return true;
807 };
808 so.rmempty = true;
809 so.cfgvalue = function(section) {
810 var macs = L.toArray(uci.get('dhcp', section, 'mac')),
811 result = [];
812
813 for (var i = 0, mac; (mac = macs[i]) != null; i++)
814 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)) {
815 var m = [
816 parseInt(RegExp.$1, 16), parseInt(RegExp.$2, 16),
817 parseInt(RegExp.$3, 16), parseInt(RegExp.$4, 16),
818 parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16)
819 ];
820
821 result.push(m.map(function(n) { return isNaN(n) ? '*' : '%02X'.format(n) }).join(':'));
822 }
823 return result.length ? result.join(' ') : null;
824 };
825 so.renderWidget = function(section_id, option_index, cfgvalue) {
826 var node = form.Value.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]),
827 ipopt = this.section.children.filter(function(o) { return o.option == 'ip' })[0];
828
829 node.addEventListener('cbi-dropdown-change', L.bind(function(ipopt, section_id, ev) {
830 var mac = ev.detail.value.value;
831 if (mac == null || mac == '' || !hosts[mac])
832 return;
833
834 var iphint = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
835 if (iphint == null)
836 return;
837
838 var ip = ipopt.formvalue(section_id);
839 if (ip != null && ip != '')
840 return;
841
842 var node = ipopt.map.findElement('id', ipopt.cbid(section_id));
843 if (node)
844 dom.callClassMethod(node, 'setValue', iphint);
845 }, this, ipopt, section_id));
846
847 return node;
848 };
849 so.validate = validateMACAddr.bind(so, pools);
850 Object.keys(hosts).forEach(function(mac) {
851 var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
852 so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
853 });
854
855 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.'));
856 so.value('ignore', _('Ignore'));
857 so.datatype = 'or(ip4addr,"ignore")';
858 so.validate = function(section, value) {
859 var m = this.section.formvalue(section, 'mac'),
860 n = this.section.formvalue(section, 'name');
861
862 if ((m == null || m == '') && (n == null || n == ''))
863 return _('One of hostname or MAC address must be specified!');
864
865 if (value == null || value == '' || value == 'ignore')
866 return true;
867
868 var leases = uci.sections('dhcp', 'host');
869
870 for (var i = 0; i < leases.length; i++)
871 if (leases[i]['.name'] != section && leases[i].ip == value)
872 return _('The IP address %h is already used by another static lease').format(value);
873
874 for (var i = 0; i < pools.length; i++) {
875 var net_mask = calculateNetwork(value, pools[i].netmask);
876
877 if (net_mask && net_mask[0] == pools[i].network)
878 return true;
879 }
880
881 return _('The IP address is outside of any DHCP pool address range');
882 };
883
884 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
885 so.value(ipv4, ipaddrs[ipv4] ? '%s (%s)'.format(ipv4, ipaddrs[ipv4]) : ipv4);
886 });
887
888 so = ss.option(form.Value, 'leasetime',
889 _('Lease time'),
890 _('Host-specific lease time, e.g. <code>5m</code>, <code>3h</code>, <code>7d</code>.'));
891 so.rmempty = true;
892 so.value('5m', _('5m (5 minutes)'));
893 so.value('3h', _('3h (3 hours)'));
894 so.value('12h', _('12h (12 hours - default)'));
895 so.value('7d', _('7d (7 days)'));
896 so.value('infinite', _('infinite (lease does not expire)'));
897
898 so = ss.option(form.Value, 'duid',
899 _('DUID'),
900 _('The DHCPv6-DUID (DHCP unique identifier) of this host.'));
901 so.datatype = 'and(rangelength(20,36),hexstring)';
902 Object.keys(duids).forEach(function(duid) {
903 so.value(duid, '%s (%s)'.format(duid, duids[duid].hostname || duids[duid].macaddr || duids[duid].ip6addr || '?'));
904 });
905
906 so = ss.option(form.Value, 'hostid',
907 _('IPv6-Suffix (hex)'),
908 _('The IPv6 interface identifier (address suffix) as hexadecimal number (max. 8 chars).'));
909 so.datatype = 'and(rangelength(0,8),hexstring)';
910
911 so = ss.option(form.DynamicList, 'tag',
912 _('Tag'),
913 _('Assign new, freeform tags to this entry.'));
914
915 so = ss.option(form.DynamicList, 'match_tag',
916 _('Match Tag'),
917 _('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 />' +
918 _('Ignore requests from unknown machines using <em>!known</em>.') + '<br /><br />' +
919 _('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.'));
920 so.value('known', _('known'));
921 so.value('!known', _('!known (not known)'));
922 so.value('known-othernet', _('known-othernet (on different subnet)'));
923 so.optional = true;
924
925 so = ss.option(form.Value, 'instance',
926 _('Instance'),
927 _('Dnsmasq instance to which this DHCP host section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
928 so.optional = true;
929
930 Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
931 so.value(index, '%s (Domain: %s, Local: %s)'.format(index, val.domain || '?', val.local || '?'));
932 });
933
934
935 so = ss.option(form.Flag, 'broadcast',
936 _('Broadcast'),
937 _('Force broadcast DHCP response.'));
938
939 so = ss.option(form.Flag, 'dns',
940 _('Forward/reverse DNS'),
941 _('Add static forward and reverse DNS entries for this host.'));
942
943 o = s.taboption('leases', CBILeaseStatus, '__status__');
944
945 if (has_dhcpv6)
946 o = s.taboption('leases', CBILease6Status, '__status6__');
947
948 return m.render().then(function(mapEl) {
949 poll.add(function() {
950 return callDHCPLeases().then(function(leaseinfo) {
951 var leases = Array.isArray(leaseinfo.dhcp_leases) ? leaseinfo.dhcp_leases : [],
952 leases6 = Array.isArray(leaseinfo.dhcp6_leases) ? leaseinfo.dhcp6_leases : [];
953
954 cbi_update_table(mapEl.querySelector('#lease_status_table'),
955 leases.map(function(lease) {
956 var exp;
957
958 if (lease.expires === false)
959 exp = E('em', _('unlimited'));
960 else if (lease.expires <= 0)
961 exp = E('em', _('expired'));
962 else
963 exp = '%t'.format(lease.expires);
964
965 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
966 name = hint ? hint.name : null,
967 host = null;
968
969 if (name && lease.hostname && lease.hostname != name)
970 host = '%s (%s)'.format(lease.hostname, name);
971 else if (lease.hostname)
972 host = lease.hostname;
973
974 return [
975 host || '-',
976 lease.ipaddr,
977 lease.macaddr,
978 exp
979 ];
980 }),
981 E('em', _('There are no active leases')));
982
983 if (has_dhcpv6) {
984 cbi_update_table(mapEl.querySelector('#lease6_status_table'),
985 leases6.map(function(lease) {
986 var exp;
987
988 if (lease.expires === false)
989 exp = E('em', _('unlimited'));
990 else if (lease.expires <= 0)
991 exp = E('em', _('expired'));
992 else
993 exp = '%t'.format(lease.expires);
994
995 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
996 name = hint ? (hint.name || L.toArray(hint.ipaddrs || hint.ipv4)[0] || L.toArray(hint.ip6addrs || hint.ipv6)[0]) : null,
997 host = null;
998
999 if (name && lease.hostname && lease.hostname != name && lease.ip6addr != name)
1000 host = '%s (%s)'.format(lease.hostname, name);
1001 else if (lease.hostname)
1002 host = lease.hostname;
1003 else if (name)
1004 host = name;
1005
1006 return [
1007 host || '-',
1008 lease.ip6addrs ? lease.ip6addrs.join(' ') : lease.ip6addr,
1009 lease.duid,
1010 exp
1011 ];
1012 }),
1013 E('em', _('There are no active leases')));
1014 }
1015 });
1016 });
1017
1018 return mapEl;
1019 });
1020 }
1021 });