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