10 'require tools.widgets as widgets';
12 var callHostHints
, callDUIDHints
, callDHCPLeases
, CBILeaseStatus
, CBILease6Status
;
14 callHostHints
= rpc
.declare({
16 method
: 'getHostHints',
20 callDUIDHints
= rpc
.declare({
22 method
: 'getDUIDHints',
26 callDHCPLeases
= rpc
.declare({
28 method
: 'getDHCPLeases',
32 CBILeaseStatus
= form
.DummyValue
.extend({
33 renderWidget: function(section_id
, option_id
, cfgvalue
) {
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'))
43 E('tr', { 'class': 'tr placeholder' }, [
44 E('td', { 'class': 'td' }, E('em', _('Collecting data...')))
51 CBILease6Status
= form
.DummyValue
.extend({
52 renderWidget: function(section_id
, option_id
, cfgvalue
) {
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'))
62 E('tr', { 'class': 'tr placeholder' }, [
63 E('td', { 'class': 'td' }, E('em', _('Collecting data...')))
70 function calculateNetwork(addr
, mask
) {
71 addr
= validation
.parseIPv4(String(addr
));
74 mask
= validation
.parseIPv4(network
.prefixToMask(+mask
));
76 mask
= validation
.parseIPv4(String(mask
));
78 if (addr
== null || mask
== null)
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)
92 function getDHCPPools() {
93 return uci
.load('dhcp').then(function() {
94 let sections
= uci
.sections('dhcp', 'dhcp'),
95 tasks
= [], pools
= [];
97 for (var i
= 0; i
< sections
.length
; i
++) {
98 if (sections
[i
].ignore
== '1' || !sections
[i
].interface)
101 tasks
.push(network
.getNetwork(sections
[i
].interface).then(L
.bind(function(section_id
, net
) {
102 var cidr
= net
? (net
.getIPAddrs()[0] || '').split('/') : null;
104 if (cidr
&& cidr
.length
== 2) {
105 var net_mask
= calculateNetwork(cidr
[0], cidr
[1]);
108 section_id
: section_id
,
109 network
: net_mask
[0],
113 }, null, sections
[i
]['.name'])));
116 return Promise
.all(tasks
).then(function() {
122 function validateHostname(sid
, s
) {
123 if (s
== null || s
== '')
127 return _('Expecting: %s').format(_('valid hostname'));
129 var labels
= s
.replace(/^\*?\.?|\.$/g, '').split(/\./);
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'));
138 function validateAddressList(sid
, s
) {
139 if (s
== null || s
== '')
142 var m
= s
.match(/^\/(.+)\/$/),
143 names
= m
? m
[1].split(/\//) : [ s
];
145 for (var i
= 0; i
< names
.length
; i
++) {
146 var res
= validateHostname(sid
, names
[i
]);
155 function validateServerSpec(sid
, s
) {
156 if (s
== null || s
== '')
159 var m
= s
.match(/^(\/.*\/)?(.*)$/);
161 return _('Expecting: %s').format(_('valid hostname'));
163 if (m
[1] != '//' && m
[1] != '/#/') {
164 var res
= validateAddressList(sid
, m
[1]);
169 if (m
[2] == '' || m
[2] == '#')
172 // ipaddr%scopeid#srvport@source@interface#srcport
174 m
= m
[2].match(/^([0-9a-f:.]+)(?:%[^#@]+)?(?:#(\d+))?(?:@([0-9a-f:.]+)(?:@[^#]+)?(?:#(\d+))?)?$/);
177 return _('Expecting: %s').format(_('valid IP address'));
179 if (validation
.parseIPv4(m
[1])) {
180 if (m
[3] != null && !validation
.parseIPv4(m
[3]))
181 return _('Expecting: %s').format(_('valid IPv4 address'));
183 else if (validation
.parseIPv6(m
[1])) {
184 if (m
[3] != null && !validation
.parseIPv6(m
[3]))
185 return _('Expecting: %s').format(_('valid IPv6 address'));
188 return _('Expecting: %s').format(_('valid IP address'));
191 if ((m
[2] != null && +m
[2] > 65535) || (m
[4] != null && +m
[4] > 65535))
192 return _('Expecting: %s').format(_('valid port value'));
197 function validateMACAddr(pools
, sid
, s
) {
198 if (s
== null || s
== '')
201 var leases
= uci
.sections('dhcp', 'host'),
202 this_macs
= L
.toArray(s
).map(function(m
) { return m
.toUpperCase() });
204 for (var i
= 0; i
< pools
.length
; i
++) {
205 var this_net_mask
= calculateNetwork(this.section
.formvalue(sid
, 'ip'), pools
[i
].netmask
);
210 for (var j
= 0; j
< leases
.length
; j
++) {
211 if (leases
[j
]['.name'] == sid
|| !leases
[j
].ip
)
214 var lease_net_mask
= calculateNetwork(leases
[j
].ip
, pools
[i
].netmask
);
216 if (!lease_net_mask
|| this_net_mask
[0] != lease_net_mask
[0])
219 var lease_macs
= L
.toArray(leases
[j
].mac
).map(function(m
) { return m
.toUpperCase() });
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
]);
237 network
.getNetworks()
241 render: function(hosts_duids_pools
) {
242 var has_dhcpv6
= L
.hasSystemFeature('dnsmasq', 'dhcpv6') || L
.hasSystemFeature('odhcpd'),
243 hosts
= hosts_duids_pools
[0],
244 duids
= hosts_duids_pools
[1],
245 pools
= hosts_duids_pools
[2],
246 networks
= hosts_duids_pools
[3],
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.'));
252 s
= m
.section(form
.TypedSection
, 'dnsmasq');
256 s
.tab('general', _('General Settings'));
257 s
.tab('advanced', _('Advanced Settings'));
258 s
.tab('leases', _('Static Leases'));
259 s
.tab('files', _('Resolv and Hosts Files'));
260 s
.tab('hosts', _('Hostnames'));
261 s
.tab('ipsets', _('IP Sets'));
262 s
.tab('relay', _('Relay'));
263 s
.tab('srvhosts', _('SRV'));
264 s
.tab('mxhosts', _('MX'));
265 s
.tab('cnamehosts', _('CNAME'));
266 s
.tab('pxe_tftp', _('PXE/TFTP Settings'));
268 s
.taboption('general', form
.Flag
, 'domainneeded',
269 _('Domain required'),
270 _('Do not forward DNS queries without dots or domain parts.'));
272 s
.taboption('general', form
.Flag
, 'authoritative',
274 _('This is the only DHCP server in the local network.'));
276 s
.taboption('general', form
.Value
, 'local',
278 _('Never forward matching domains and subdomains, resolve from DHCP or hosts files only.'));
280 s
.taboption('general', form
.Value
, 'domain',
282 _('Local domain suffix appended to DHCP names and hosts file entries.'));
284 o
= s
.taboption('general', form
.Flag
, 'logqueries',
286 _('Write received DNS queries to syslog.'));
289 o
= s
.taboption('general', form
.DynamicList
, 'server',
290 _('DNS forwardings'),
291 _('List of upstream resolvers to forward queries to.'));
293 o
.placeholder
= '/example.org/10.1.2.3';
294 o
.validate
= validateServerSpec
;
296 o
= s
.taboption('general', form
.DynamicList
, 'address',
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.'));
303 o
.placeholder
= '/router.local/router.lan/192.168.0.1';
305 o
= s
.taboption('general', form
.DynamicList
, 'ipset',
307 _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.'));
309 o
.placeholder
= '/example.org/ipset,ipset6';
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'));
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');
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');
326 o
.placeholder
= 'ihost.netflix.com';
327 o
.validate
= validateAddressList
;
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.'));
335 o
= s
.taboption('general', form
.Flag
, 'nonwildcard',
337 _('Bind dynamically to interfaces rather than wildcard address.'));
338 o
.default = o
.enabled
;
342 o
= s
.taboption('general', form
.DynamicList
, 'interface',
343 _('Listen interfaces'),
344 _('Listen only on the specified interfaces, and loopback if not excluded explicitly.'));
346 o
.placeholder
= 'lan';
348 o
= s
.taboption('general', form
.DynamicList
, 'notinterface',
349 _('Exclude interfaces'),
350 _('Do not listen on the specified interfaces.'));
352 o
.placeholder
= 'loopback';
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.'));
365 ss
.nodescriptions
= true;
367 so
= ss
.option(form
.Value
, 'local_addr', _('Relay from'));
369 so
.datatype
= 'ipaddr';
371 for (var family
= 4; family
<= 6; family
+= 2) {
372 for (var i
= 0; i
< networks
.length
; i
++) {
373 if (networks
[i
].getName() != 'loopback') {
374 var addrs
= (family
== 6) ? networks
[i
].getIP6Addrs() : networks
[i
].getIPAddrs();
375 for (var j
= 0; j
< addrs
.length
; j
++) {
376 var addr
= addrs
[j
].split('/')[0];
377 so
.value(addr
, E([], [
379 widgets
.NetworkSelect
.prototype.renderIfaceBadge(networks
[i
]),
387 so
= ss
.option(form
.Value
, 'server_addr', _('Relay to address'));
390 so
.placeholder
= '192.168.10.1#535';
392 so
.validate = function(section
, value
) {
393 var m
= this.section
.formvalue(section
, 'local_addr'),
394 n
= this.section
.formvalue(section
, 'server_addr'),
396 if (n
!= null && n
!= '')
398 if (p
.length
> 1 && !/^[0-9]+$/.test(p
[1]))
399 return _('Expected port number.');
403 if ((m
== null || m
== '') && (n
== null || n
== ''))
404 return _('Both "Relay from" and "Relay to address" must be specified.');
406 if ((validation
.parseIPv6(m
) && validation
.parseIPv6(n
)) ||
407 validation
.parseIPv4(m
) && validation
.parseIPv4(n
))
410 return _('Address families of "Relay from" and "Relay to address" must match.')
413 so
= ss
.option(widgets
.NetworkSelect
, 'interface', _('Only accept replies via'));
416 so
.placeholder
= 'lan';
418 s
.taboption('files', form
.Flag
, 'readethers',
419 _('Use <code>/etc/ethers</code>'),
420 _('Read <code>/etc/ethers</code> to configure the DHCP server.'));
422 s
.taboption('files', form
.Value
, 'leasefile',
424 _('File to store DHCP lease information.'));
426 o
= s
.taboption('files', form
.Flag
, 'noresolv',
427 _('Ignore resolv file'));
430 o
= s
.taboption('files', form
.Value
, 'resolvfile',
432 _('File with upstream resolvers.'));
433 o
.depends('noresolv', '0');
434 o
.placeholder
= '/tmp/resolv.conf.d/resolv.conf.auto';
437 o
= s
.taboption('files', form
.Flag
, 'nohosts',
438 _('Ignore <code>/etc/hosts</code>'));
441 o
= s
.taboption('files', form
.DynamicList
, 'addnhosts',
442 _('Additional hosts files'));
444 o
.placeholder
= '/etc/dnsmasq.hosts';
446 o
= s
.taboption('advanced', form
.Flag
, 'quietdhcp',
447 _('Suppress logging'),
448 _('Suppress logging of the routine operation for the DHCP protocol.'));
451 o
= s
.taboption('advanced', form
.Flag
, 'sequential_ip',
452 _('Allocate IPs sequentially'),
453 _('Allocate IP addresses sequentially, starting from the lowest available address.'));
456 o
= s
.taboption('advanced', form
.Flag
, 'boguspriv',
458 _('Do not forward reverse lookups for local networks.'));
459 o
.default = o
.enabled
;
461 s
.taboption('advanced', form
.Flag
, 'filterwin2k',
462 _('Filter SRV/SOA service discovery'),
463 _('Filters SRV/SOA service discovery, to avoid triggering dial-on-demand links.') + '<br />' +
464 _('May prevent VoIP or other services from working.'));
466 o
= s
.taboption('advanced', form
.Flag
, 'filter_aaaa',
467 _('Filter IPv6 AAAA records'),
468 _('Remove IPv6 addresses from the results and only return IPv4 addresses.') + '<br />' +
469 _('Can be useful if ISP has IPv6 nameservers but does not provide IPv6 routing.'));
472 o
= s
.taboption('advanced', form
.Flag
, 'filter_a',
473 _('Filter IPv4 A records'),
474 _('Remove IPv4 addresses from the results and only return IPv6 addresses.'));
477 s
.taboption('advanced', form
.Flag
, 'localise_queries',
478 _('Localise queries'),
479 _('Return answers to DNS queries matching the subnet from which the query was received if multiple IPs are available.'));
481 if (L
.hasSystemFeature('dnsmasq', 'dnssec')) {
482 o
= s
.taboption('advanced', form
.Flag
, 'dnssec',
484 _('Validate DNS replies and cache DNSSEC data, requires upstream to support DNSSEC.'));
487 o
= s
.taboption('advanced', form
.Flag
, 'dnsseccheckunsigned',
488 _('DNSSEC check unsigned'),
489 _('Verify unsigned domain responses really come from unsigned domains.'));
490 o
.default = o
.enabled
;
494 s
.taboption('advanced', form
.Flag
, 'expandhosts',
496 _('Add local domain suffix to names served from hosts files.'));
498 s
.taboption('advanced', form
.Flag
, 'nonegcache',
499 _('No negative cache'),
500 _('Do not cache negative replies, e.g. for non-existent domains.'));
502 o
= s
.taboption('advanced', form
.Value
, 'serversfile',
503 _('Additional servers file'),
504 _('File listing upstream resolvers, optionally domain-specific, e.g. <code>server=1.2.3.4</code>, <code>server=/domain/1.2.3.4</code>.'));
505 o
.placeholder
= '/etc/dnsmasq.servers';
507 o
= s
.taboption('advanced', form
.Flag
, 'strictorder',
509 _('Upstream resolvers will be queried in the order of the resolv file.'));
512 o
= s
.taboption('advanced', form
.Flag
, 'allservers',
514 _('Query all available upstream resolvers.'));
517 o
= s
.taboption('advanced', form
.DynamicList
, 'bogusnxdomain',
518 _('IPs to override with NXDOMAIN'),
519 _('List of IP addresses to convert into NXDOMAIN responses.'));
521 o
.placeholder
= '64.94.110.11';
523 o
= s
.taboption('advanced', form
.Value
, 'port',
524 _('DNS server port'),
525 _('Listening port for inbound DNS queries.'));
530 o
= s
.taboption('advanced', form
.Value
, 'queryport',
532 _('Fixed source port for outbound DNS queries.'));
535 o
.placeholder
= _('any');
537 o
= s
.taboption('advanced', form
.Value
, 'dhcpleasemax',
538 _('Max. DHCP leases'),
539 _('Maximum allowed number of active DHCP leases.'));
541 o
.datatype
= 'uinteger';
542 o
.placeholder
= _('unlimited');
544 o
= s
.taboption('advanced', form
.Value
, 'ednspacket_max',
545 _('Max. EDNS0 packet size'),
546 _('Maximum allowed size of EDNS0 UDP packets.'));
548 o
.datatype
= 'uinteger';
549 o
.placeholder
= 1280;
551 o
= s
.taboption('advanced', form
.Value
, 'dnsforwardmax',
552 _('Max. concurrent queries'),
553 _('Maximum allowed number of concurrent DNS queries.'));
555 o
.datatype
= 'uinteger';
558 o
= s
.taboption('advanced', form
.Value
, 'cachesize',
559 _('Size of DNS query cache'),
560 _('Number of cached DNS entries, 10000 is maximum, 0 is no caching.'));
562 o
.datatype
= 'range(0,10000)';
563 o
.placeholder
= 1000;
565 o
= s
.taboption('pxe_tftp', form
.Flag
, 'enable_tftp',
566 _('Enable TFTP server'),
567 _('Enable the built-in single-instance TFTP server.'));
570 o
= s
.taboption('pxe_tftp', form
.Value
, 'tftp_root',
571 _('TFTP server root'),
572 _('Root directory for files served via TFTP. <em>Enable TFTP server</em> and <em>TFTP server root</em> turn on the TFTP server and serve files from <em>TFTP server root</em>.'));
573 o
.depends('enable_tftp', '1');
577 o
= s
.taboption('pxe_tftp', form
.Value
, 'dhcp_boot',
578 _('Network boot image'),
579 _('Filename of the boot image advertised to clients.'));
580 o
.depends('enable_tftp', '1');
582 o
.placeholder
= 'pxelinux.0';
584 /* PXE - https://openwrt.org/docs/guide-user/base-system/dhcp#booting_options */
585 o = s.taboption('pxe_tftp', form.SectionValue, '__pxe__', form.GridSection, 'boot', null,
586 _('Special <abbr title="Preboot eXecution Environment">PXE</abbr> boot options for Dnsmasq.'));
590 ss.nodescriptions = true;
592 so = ss.option(form.Value, 'filename',
594 _('Host requests this filename from the boot server.'));
596 so.placeholder = 'pxelinux.0';
598 so = ss.option(form.Value, 'servername',
600 _('The hostname of the boot server'));
602 so.placeholder = 'myNAS';
604 so = ss.option(form.Value, 'serveraddress',
606 _('The IP address of the boot server'));
608 so.placeholder = '192.168.1.2';
610 so = ss.option(form.DynamicList, 'dhcp_option',
612 _('Options for the Network-ID. (Note: needs also Network-ID.) E.g. "<code>42,192.168.1.4</code>" for NTP server, "<code>3,192.168.4.4</code>" for default route. <code>0.0.0.0</code> means "the address of the system running dnsmasq".'));
614 so.placeholder = '42,192.168.1.4';
616 so = ss.option(widgets.DeviceSelect, 'networkid',
618 _('Apply DHCP Options to this net. (Empty = all clients).'));
622 so = ss.option(form.Flag, 'force',
624 _('Always send DHCP Options. Sometimes needed, with e.g. PXELinux.'));
627 so = ss.option(form.Value, 'instance',
629 _('Dnsmasq instance to which this boot section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
632 Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
633 so.value(index, '%s (Domain: %s, Local: %s)'.format(index, val.domain || '?', val.local || '?'));
636 o = s.taboption('srvhosts', form.SectionValue, '__srvhosts__', form.TableSection, 'srvhost', null,
637 _('Bind service records to a domain name: specify the location of services. See <a href="%s">RFC2782</a>.').format('https://datatracker.ietf.org/doc/html/rfc2782')
638 + '<br />' + _('_service: _sip, _ldap, _imap, _stun, _xmpp-client, … . (Note: while _http is possible, no browsers support SRV records.)')
639 + '<br />' + _('_proto: _tcp, _udp, _sctp, _quic, … .')
640 + '<br />' + _('You may add multiple records for the same Target.')
641 + '<br />' + _('Larger weights (of the same prio) are given a proportionately higher probability of being selected.'));
650 so = ss.option(form.Value, 'srv', _('SRV'), _('Syntax: <code>_service._proto.example.com</code>.'));
652 so.datatype = 'hostname';
653 so.placeholder = '_sip._tcp.example.com';
655 so = ss.option(form.Value, 'target', _('Target'), _('CNAME or fqdn'));
657 so.datatype = 'hostname';
658 so.placeholder = 'sip.example.com';
660 so = ss.option(form.Value, 'port', _('Port'));
662 so.datatype = 'port';
663 so.placeholder = '5060';
665 so = ss.option(form.Value, 'class', _('Priority'), _('Ordinal: lower comes first.'));
667 so.datatype = 'range(0,65535)';
668 so.placeholder = '10';
670 so = ss.option(form.Value, 'weight', _('Weight'));
672 so.datatype = 'range(0,65535)';
673 so.placeholder = '50';
675 o = s.taboption('mxhosts', form.SectionValue, '__mxhosts__', form.TableSection, 'mxhost', null,
676 _('Bind service records to a domain name: specify the location of services.')
677 + '<br />' + _('You may add multiple records for the same domain.'));
685 ss.nodescriptions = true;
687 so = ss.option(form.Value, 'domain', _('Domain'));
689 so.datatype = 'hostname';
690 so.placeholder = 'example.com';
692 so = ss.option(form.Value, 'relay', _('Relay'));
694 so.datatype = 'hostname';
695 so.placeholder = 'relay.example.com';
697 so = ss.option(form.Value, 'pref', _('Priority'), _('Ordinal: lower comes first.'));
699 so.datatype = 'range(0,65535)';
700 so.placeholder = '0';
702 o = s.taboption('cnamehosts', form.SectionValue, '__cname__', form.TableSection, 'cname', null,
703 _('Set an alias for a hostname.'));
711 ss.nodescriptions = true;
713 so = ss.option(form.Value, 'cname', _('Domain'));
715 so.datatype = 'hostname';
716 so.placeholder = 'www.example.com';
718 so = ss.option(form.Value, 'target', _('Target'));
720 so.datatype = 'hostname';
721 so.placeholder = 'example.com';
723 o = s.taboption('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
724 _('Hostnames are used to bind a domain name to an IP address. This setting is redundant for hostnames already configured with static leases, but it can be useful to rebind an FQDN.'));
732 so = ss.option(form.Value, 'name', _('Hostname'));
734 so.datatype = 'hostname';
736 so = ss.option(form.Value, 'ip', _('IP address'));
738 so.datatype = 'ipaddr';
742 Object.keys(hosts).forEach(function(mac) {
743 var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4);
745 for (var i = 0; i < addrs.length; i++)
746 ipaddrs[addrs[i]] = hosts[mac].name || mac;
749 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
750 so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
753 o = s.taboption('ipsets', form.SectionValue, '__ipsets__', form.GridSection, 'ipset', null,
754 _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.'));
762 so = ss.option(form.DynamicList, 'name', _('IP set'));
764 so.datatype = 'string';
766 so = ss.option(form.DynamicList, 'domain', _('Domain'));
768 so.datatype = 'hostname';
770 o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
771 _('Static leases are used to assign fixed IP addresses and symbolic hostnames to DHCP clients. They are also required for non-dynamic interface configurations where only hosts with a corresponding lease are served.') + '<br /><br />' +
772 _('Use the <em>Add</em> Button to add a new lease entry. The <em>MAC address</em> identifies the host, the <em>IPv4 address</em> specifies the fixed address to use, and the <em>Hostname</em> is assigned as a symbolic name to the requesting host. The optional <em>Lease time</em> can be used to set non-standard host-specific lease time, e.g. 12h, 3d or infinite.') + '<br /><br />' +
773 _('The tag construct filters which host directives are used; more than one tag can be provided, in this case the request must match all of them. Tagged directives are used in preference to untagged ones. Note that one of mac, duid or hostname still needs to be specified (can be a wildcard).'));
780 ss.nodescriptions = true;
782 ss.modaltitle = _('Edit static lease');
784 so = ss.option(form.Value, 'name',
786 _('Optional hostname to assign'));
787 so.validate = validateHostname;
789 so.write = function(section, value) {
790 uci.set('dhcp', section, 'name', value);
791 uci.set('dhcp', section, 'dns', '1');
793 so.remove = function(section) {
794 uci.unset('dhcp', section, 'name');
795 uci.unset('dhcp', section, 'dns');
798 so = ss.option(form.Value, 'mac',
799 _('MAC address(es)'),
800 _('The hardware address(es) of this entry/host, separated by spaces.') + '<br /><br />' +
801 _('In DHCPv4, it is possible to include more than one mac address. This allows an IP address to be associated with multiple macaddrs, and dnsmasq abandons a DHCP lease to one of the macaddrs when another asks for a lease. It only works reliably if only one of the macaddrs is active at any time.'));
802 //As a special case, in DHCPv4, it is possible to include more than one hardware address. eg: --dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.2 This allows an IP address to be associated with multiple hardware addresses, and gives dnsmasq permission to abandon a DHCP lease to one of the hardware addresses when another one asks for a lease
803 so.validate = function(section_id, value) {
804 var macaddrs = L.toArray(value);
806 for (var i = 0; i < macaddrs.length; i++)
807 if (!macaddrs[i].match(/^([a-fA-F0-9]{2}|\*):([a-fA-F0-9]{2}:|\*:){4}(?:[a-fA-F0-9]{2}|\*)$/))
808 return _('Expecting a valid MAC address, optionally including wildcards');
813 so.cfgvalue = function(section) {
814 var macs = L.toArray(uci.get('dhcp', section, 'mac')),
817 for (var i = 0, mac; (mac = macs[i]) != null; i++)
818 if (/^([0-9a-fA-F]{1,2}|\*):([0-9a-fA-F]{1,2}|\*):([0-9a-fA-F]{1,2}|\*):([0-9a-fA-F]{1,2}|\*):([0-9a-fA-F]{1,2}|\*):([0-9a-fA-F]{1,2}|\*)$/.test(mac)) {
820 parseInt(RegExp.$1, 16), parseInt(RegExp.$2, 16),
821 parseInt(RegExp.$3, 16), parseInt(RegExp.$4, 16),
822 parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16)
825 result.push(m.map(function(n) { return isNaN(n) ? '*' : '%02X'.format(n) }).join(':'));
827 return result.length ? result.join(' ') : null;
829 so.renderWidget = function(section_id, option_index, cfgvalue) {
830 var node = form.Value.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]),
831 ipopt = this.section.children.filter(function(o) { return o.option == 'ip' })[0];
833 node.addEventListener('cbi-dropdown-change', L.bind(function(ipopt, section_id, ev) {
834 var mac = ev.detail.value.value;
835 if (mac == null || mac == '' || !hosts[mac])
838 var iphint = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
842 var ip = ipopt.formvalue(section_id);
843 if (ip != null && ip != '')
846 var node = ipopt.map.findElement('id', ipopt.cbid(section_id));
848 dom.callClassMethod(node, 'setValue', iphint);
849 }, this, ipopt, section_id));
853 so.validate = validateMACAddr.bind(so, pools);
854 Object.keys(hosts).forEach(function(mac) {
855 var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
856 so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
859 so = ss.option(form.Value, 'ip', _('IPv4 address'), _('The IP address to be used for this host, or <em>ignore</em> to ignore any DHCP request from this host.'));
860 so.value('ignore', _('Ignore'));
861 so.datatype = 'or(ip4addr,"ignore")';
862 so.validate = function(section, value) {
863 var m = this.section.formvalue(section, 'mac'),
864 n = this.section.formvalue(section, 'name');
866 if ((m == null || m == '') && (n == null || n == ''))
867 return _('One of hostname or MAC address must be specified!');
869 if (value == null || value == '' || value == 'ignore')
872 var leases = uci.sections('dhcp', 'host');
874 for (var i = 0; i < leases.length; i++)
875 if (leases[i]['.name'] != section && leases[i].ip == value)
876 return _('The IP address %h is already used by another static lease').format(value);
878 for (var i = 0; i < pools.length; i++) {
879 var net_mask = calculateNetwork(value, pools[i].netmask);
881 if (net_mask && net_mask[0] == pools[i].network)
885 return _('The IP address is outside of any DHCP pool address range');
888 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
889 so.value(ipv4, ipaddrs[ipv4] ? '%s (%s)'.format(ipv4, ipaddrs[ipv4]) : ipv4);
892 so = ss.option(form.Value, 'leasetime',
894 _('Host-specific lease time, e.g. <code>5m</code>, <code>3h</code>, <code>7d</code>.'));
896 so.value('5m', _('5m (5 minutes)'));
897 so.value('3h', _('3h (3 hours)'));
898 so.value('12h', _('12h (12 hours - default)'));
899 so.value('7d', _('7d (7 days)'));
900 so.value('infinite', _('infinite (lease does not expire)'));
902 so = ss.option(form.Value, 'duid',
904 _('The DHCPv6-DUID (DHCP unique identifier) of this host.'));
905 so.datatype = 'and(rangelength(20,36),hexstring)';
906 Object.keys(duids).forEach(function(duid) {
907 so.value(duid, '%s (%s)'.format(duid, duids[duid].hostname || duids[duid].macaddr || duids[duid].ip6addr || '?'));
910 so = ss.option(form.Value, 'hostid',
911 _('IPv6-Suffix (hex)'),
912 _('The IPv6 interface identifier (address suffix) as hexadecimal number (max. 16 chars).'));
913 so.datatype = 'and(rangelength(0,16),hexstring)';
915 so = ss.option(form.DynamicList, 'tag',
917 _('Assign new, freeform tags to this entry.'));
919 so = ss.option(form.DynamicList, 'match_tag',
921 _('When a host matches an entry then the special tag <em>known</em> is set. Use <em>known</em> to match all known hosts.') + '<br /><br />' +
922 _('Ignore requests from unknown machines using <em>!known</em>.') + '<br /><br />' +
923 _('If a host matches an entry which cannot be used because it specifies an address on a different subnet, the tag <em>known-othernet</em> is set.'));
924 so.value('known', _('known'));
925 so.value('!known', _('!known (not known)'));
926 so.value('known-othernet', _('known-othernet (on different subnet)'));
929 so = ss.option(form.Value, 'instance',
931 _('Dnsmasq instance to which this DHCP host section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
934 Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
935 so.value(index, '%s (Domain: %s, Local: %s)'.format(index, val.domain || '?', val.local || '?'));
939 so = ss.option(form.Flag, 'broadcast',
941 _('Force broadcast DHCP response.'));
943 so = ss.option(form.Flag, 'dns',
944 _('Forward/reverse DNS'),
945 _('Add static forward and reverse DNS entries for this host.'));
947 o = s.taboption('leases', CBILeaseStatus, '__status__');
950 o = s.taboption('leases', CBILease6Status, '__status6__');
952 return m.render().then(function(mapEl) {
953 poll.add(function() {
954 return callDHCPLeases().then(function(leaseinfo) {
955 var leases = Array.isArray(leaseinfo.dhcp_leases) ? leaseinfo.dhcp_leases : [],
956 leases6 = Array.isArray(leaseinfo.dhcp6_leases) ? leaseinfo.dhcp6_leases : [];
958 cbi_update_table(mapEl.querySelector('#lease_status_table'),
959 leases.map(function(lease) {
962 if (lease.expires === false)
963 exp = E('em', _('unlimited'));
964 else if (lease.expires <= 0)
965 exp = E('em', _('expired'));
967 exp = '%t'.format(lease.expires);
969 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
970 name = hint ? hint.name : null,
973 if (name && lease.hostname && lease.hostname != name)
974 host = '%s (%s)'.format(lease.hostname, name);
975 else if (lease.hostname)
976 host = lease.hostname;
985 E('em', _('There are no active leases')));
988 cbi_update_table(mapEl.querySelector('#lease6_status_table'),
989 leases6.map(function(lease) {
992 if (lease.expires === false)
993 exp = E('em', _('unlimited'));
994 else if (lease.expires <= 0)
995 exp = E('em', _('expired'));
997 exp = '%t'.format(lease.expires);
999 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
1000 name = hint ? (hint.name || L.toArray(hint.ipaddrs || hint.ipv4)[0] || L.toArray(hint.ip6addrs || hint.ipv6)[0]) : null,
1003 if (name && lease.hostname && lease.hostname != name && lease.ip6addr != name)
1004 host = '%s (%s)'.format(lease.hostname, name);
1005 else if (lease.hostname)
1006 host = lease.hostname;
1012 lease.ip6addrs ? lease.ip6addrs.join(' ') : lease.ip6addr,
1017 E('em', _('There are no active leases')));