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