luci-mod-network: use network selector for dnsmasq instances
[project/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / view / network / dhcp.js
index cb1aa994725af43e49a509fdf585a4ff8824c65d..6efd94e6a08038eeaeecd86b807123f169aaddf3 100644 (file)
@@ -7,6 +7,7 @@
 'require form';
 'require network';
 'require validation';
+'require tools.widgets as widgets';
 
 var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus, CBILease6Status;
 
@@ -125,7 +126,7 @@ function validateHostname(sid, s) {
        if (s.length > 256)
                return _('Expecting: %s').format(_('valid hostname'));
 
-       var labels = s.replace(/^\.+|\.$/g, '').split(/\./);
+       var labels = s.replace(/^\*?\.?|\.$/g, '').split(/\./);
 
        for (var i = 0; i < labels.length; i++)
                if (!labels[i].match(/^[a-z0-9_](?:[a-z0-9-]{0,61}[a-z0-9])?$/i))
@@ -155,13 +156,15 @@ function validateServerSpec(sid, s) {
        if (s == null || s == '')
                return true;
 
-       var m = s.match(/^(?:\/(.+)\/)?(.*)$/);
+       var m = s.match(/^(\/.*\/)?(.*)$/);
        if (!m)
                return _('Expecting: %s').format(_('valid hostname'));
 
-       var res = validateAddressList(sid, m[1]);
-       if (res !== true)
-               return res;
+       if (m[1] != '//' && m[1] != '/#/') {
+               var res = validateAddressList(sid, m[1]);
+               if (res !== true)
+                       return res;
+       }
 
        if (m[2] == '' || m[2] == '#')
                return true;
@@ -230,7 +233,8 @@ return view.extend({
                return Promise.all([
                        callHostHints(),
                        callDUIDHints(),
-                       getDHCPPools()
+                       getDHCPPools(),
+                       network.getDevices()
                ]);
        },
 
@@ -239,273 +243,544 @@ return view.extend({
                    hosts = hosts_duids_pools[0],
                    duids = hosts_duids_pools[1],
                    pools = hosts_duids_pools[2],
+                   ndevs = hosts_duids_pools[3],
                    m, s, o, ss, so;
 
-               m = new form.Map('dhcp', _('DHCP and DNS'), _('Dnsmasq is a combined <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-Server and <abbr title="Domain Name System">DNS</abbr>-Forwarder for <abbr title="Network Address Translation">NAT</abbr> firewalls'));
+               m = new form.Map('dhcp', _('DHCP and DNS'),
+                       _('Dnsmasq is a lightweight <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> server and <abbr title="Domain Name System">DNS</abbr> forwarder.'));
 
-               s = m.section(form.TypedSection, 'dnsmasq', _('Server Settings'));
+               s = m.section(form.TypedSection, 'dnsmasq');
                s.anonymous = true;
                s.addremove = false;
 
                s.tab('general', _('General Settings'));
-               s.tab('files', _('Resolv and Hosts Files'));
-               s.tab('tftp', _('TFTP Settings'));
                s.tab('advanced', _('Advanced Settings'));
                s.tab('leases', _('Static Leases'));
+               s.tab('files', _('Resolv and Hosts Files'));
+               s.tab('hosts', _('Hostnames'));
+               s.tab('ipsets', _('IP Sets'));
+               s.tab('relay', _('Relay'));
+               s.tab('srvhosts', _('SRV'));
+               s.tab('mxhosts', _('MX'));
+               s.tab('cnamehosts', _('CNAME'));
+               s.tab('pxe_tftp', _('PXE/TFTP Settings'));
 
                s.taboption('general', form.Flag, 'domainneeded',
                        _('Domain required'),
-                       _('Don\'t forward <abbr title="Domain Name System">DNS</abbr>-Requests without <abbr title="Domain Name System">DNS</abbr>-Name'));
+                       _('Do not forward DNS queries without dots or domain parts.'));
 
                s.taboption('general', form.Flag, 'authoritative',
                        _('Authoritative'),
-                       _('This is the only <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> in the local network'));
+                       _('This is the only DHCP server in the local network.'));
+
+               s.taboption('general', form.Value, 'local',
+                       _('Local server'),
+                       _('Never forward matching domains and subdomains, resolve from DHCP or hosts files only.'));
+
+               s.taboption('general', form.Value, 'domain',
+                       _('Local domain'),
+                       _('Local domain suffix appended to DHCP names and hosts file entries.'));
+
+               o = s.taboption('general', form.Flag, 'logqueries',
+                       _('Log queries'),
+                       _('Write received DNS queries to syslog.'));
+               o.optional = true;
+
+               o = s.taboption('general', form.DynamicList, 'server',
+                       _('DNS forwardings'),
+                       _('List of upstream resolvers to forward queries to.'));
+               o.optional = true;
+               o.placeholder = '/example.org/10.1.2.3';
+               o.validate = validateServerSpec;
+
+               o = s.taboption('general', form.DynamicList, 'address',
+                       _('Addresses'),
+                       _('Resolve specified FQDNs to an IP.') + '<br />' +
+                       _('Syntax: <code>/fqdn[/fqdn…]/[ipaddr]</code>.') + '<br />' +
+                       _('<code>/#/</code> matches any domain. <code>/example.com/</code> returns NXDOMAIN.') + '<br />' +
+                       _('<code>/example.com/#</code> returns NULL addresses (<code>0.0.0.0</code> and <code>::</code>) for example.com and its subdomains.'));
+               o.optional = true;
+               o.placeholder = '/router.local/router.lan/192.168.0.1';
+
+               o = s.taboption('general', form.DynamicList, 'ipset',
+                       _('IP sets'),
+                       _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.'));
+               o.optional = true;
+               o.placeholder = '/example.org/ipset,ipset6';
+
+               o = s.taboption('general', form.Flag, 'rebind_protection',
+                       _('Rebind protection'),
+                       _('Discard upstream responses containing <a href="%s">RFC1918</a> addresses.').format('https://datatracker.ietf.org/doc/html/rfc1918'));
+               o.rmempty = false;
+
+               o = s.taboption('general', form.Flag, 'rebind_localhost',
+                       _('Allow localhost'),
+                       _('Exempt <code>127.0.0.0/8</code> and <code>::1</code> from rebinding checks, e.g. for RBL services.'));
+               o.depends('rebind_protection', '1');
+
+               o = s.taboption('general', form.DynamicList, 'rebind_domain',
+                       _('Domain whitelist'),
+                       _('List of domains to allow RFC1918 responses for.'));
+               o.depends('rebind_protection', '1');
+               o.optional = true;
+               o.placeholder = 'ihost.netflix.com';
+               o.validate = validateAddressList;
+
+               o = s.taboption('general', form.Flag, 'localservice',
+                       _('Local service only'),
+                       _('Accept DNS queries only from hosts whose address is on a local subnet.'));
+               o.optional = false;
+               o.rmempty = false;
+
+               o = s.taboption('general', form.Flag, 'nonwildcard',
+                       _('Non-wildcard'),
+                       _('Bind dynamically to interfaces rather than wildcard address.'));
+               o.default = o.enabled;
+               o.optional = false;
+               o.rmempty = true;
+
+               o = s.taboption('general', widgets.NetworkSelect, 'interface',
+                       _('Listen interfaces'),
+                       _('Listen only on the specified interfaces, and loopback if not excluded explicitly.'));
+               o.multiple = true;
+               o.nocreate = true;
+
+               o = s.taboption('general', widgets.NetworkSelect, 'notinterface',
+                       _('Exclude interfaces'),
+                       _('Do not listen on the specified interfaces.'));
+               o.loopback = true;
+               o.multiple = true;
+               o.nocreate = true;
+
+               o = s.taboption('relay', form.SectionValue, '__relays__', form.TableSection, 'relay', null,
+                       _('Relay DHCP requests elsewhere. OK: v4↔v4, v6↔v6. Not OK: v4↔v6, v6↔v4.')
+                       + '<br />' + _('Note: you may also need a DHCP Proxy (currently unavailable) when specifying a non-standard Relay To port(<code>addr#port</code>).')
+                       + '<br />' + _('You may add multiple unique Relay To on the same Listen addr.'));
+
+               ss = o.subsection;
+
+               ss.addremove = true;
+               ss.anonymous = true;
+               ss.sortable  = true;
+               ss.rowcolors = true;
+               ss.nodescriptions = true;
+
+               so = ss.option(form.Value, 'id', _('ID'));
+               so.rmempty = false;
+               so.optional = true;
+
+               so = ss.option(widgets.NetworkSelect, 'interface', _('Interface'));
+               so.optional = true;
+               so.rmempty = false;
+               so.placeholder = 'lan';
+
+               so = ss.option(form.Value, 'local_addr', _('Listen address'));
+               so.rmempty = false;
+               so.datatype = 'ipaddr';
+
+               for (var family = 4; family <= 6; family += 2) {
+                       for (var i = 0; i < ndevs.length; i++) {
+                               var addrs = (family == 6) ? ndevs[i].getIP6Addrs() : ndevs[i].getIPAddrs();
+                               for (var j = 0; j < addrs.length; j++)
+                                       so.value(addrs[j].split('/')[0]);
+                       }
+               }
+
+               so = ss.option(form.Value, 'server_addr', _('Relay To address'));
+               so.rmempty = false;
+               so.optional = false;
+               so.placeholder = '192.168.10.1#535';
+
+               so.validate = function(section, value) {
+                       var m = this.section.formvalue(section, 'local_addr'),
+                           n = this.section.formvalue(section, 'server_addr'),
+                           p;
+                       if (n != null && n != '')
+                           p = n.split('#');
+                               if (p.length > 1 && !/^[0-9]+$/.test(p[1]))
+                                       return _('Expected port number.');
+                               else
+                                       n = p[0];
+
+                       if ((m == null || m == '') && (n == null || n == ''))
+                               return _('Both Listen addr and Relay To must be specified.');
 
+                       if ((validation.parseIPv6(m) && validation.parseIPv6(n)) ||
+                               validation.parseIPv4(m) && validation.parseIPv4(n))
+                               return true;
+                       else
+                               return _('Listen and Relay To IP family must be homogeneous.')
+               };
 
                s.taboption('files', form.Flag, 'readethers',
                        _('Use <code>/etc/ethers</code>'),
-                       _('Read <code>/etc/ethers</code> to configure the <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-Server'));
+                       _('Read <code>/etc/ethers</code> to configure the DHCP server.'));
 
                s.taboption('files', form.Value, 'leasefile',
-                       _('Leasefile'),
-                       _('file where given <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-leases will be stored'));
+                       _('Lease file'),
+                       _('File to store DHCP lease information.'));
 
-               s.taboption('files', form.Flag, 'noresolv',
-                       _('Ignore resolve file')).optional = true;
+               o = s.taboption('files', form.Flag, 'noresolv',
+                       _('Ignore resolv file'));
+               o.optional = true;
 
                o = s.taboption('files', form.Value, 'resolvfile',
-                       _('Resolve file'),
-                       _('local <abbr title="Domain Name System">DNS</abbr> file'));
-
+                       _('Resolv file'),
+                       _('File with upstream resolvers.'));
                o.depends('noresolv', '0');
                o.placeholder = '/tmp/resolv.conf.d/resolv.conf.auto';
                o.optional = true;
 
+               o = s.taboption('files', form.Flag, 'nohosts',
+                       _('Ignore <code>/etc/hosts</code>'));
+               o.optional = true;
 
-               s.taboption('files', form.Flag, 'nohosts',
-                       _('Ignore <code>/etc/hosts</code>')).optional = true;
-
-               s.taboption('files', form.DynamicList, 'addnhosts',
-                       _('Additional Hosts files')).optional = true;
+               o = s.taboption('files', form.DynamicList, 'addnhosts',
+                       _('Additional hosts files'));
+               o.optional = true;
+               o.placeholder = '/etc/dnsmasq.hosts';
 
                o = s.taboption('advanced', form.Flag, 'quietdhcp',
                        _('Suppress logging'),
-                       _('Suppress logging of the routine operation of these protocols'));
+                       _('Suppress logging of the routine operation for the DHCP protocol.'));
                o.optional = true;
 
                o = s.taboption('advanced', form.Flag, 'sequential_ip',
-                       _('Allocate IP sequentially'),
-                       _('Allocate IP addresses sequentially, starting from the lowest available address'));
+                       _('Allocate IPs sequentially'),
+                       _('Allocate IP addresses sequentially, starting from the lowest available address.'));
                o.optional = true;
 
                o = s.taboption('advanced', form.Flag, 'boguspriv',
                        _('Filter private'),
-                       _('Do not forward reverse lookups for local networks'));
+                       _('Do not forward reverse lookups for local networks.'));
                o.default = o.enabled;
 
                s.taboption('advanced', form.Flag, 'filterwin2k',
-                       _('Filter useless'),
-                       _('Do not forward requests that cannot be answered by public name servers'));
+                       _('Filter SRV/SOA service discovery'),
+                       _('Filters SRV/SOA service discovery, to avoid triggering dial-on-demand links.') + '<br />' +
+                       _('May prevent VoIP or other services from working.'));
+
+               o = s.taboption('advanced', form.Flag, 'filter_aaaa',
+                       _('Filter IPv6 AAAA records'),
+                       _('Remove IPv6 addresses from the results and only return IPv4 addresses.') + '<br />' +
+                       _('Can be useful if ISP has IPv6 nameservers but does not provide IPv6 routing.'));
+               o.optional = true;
 
+               o = s.taboption('advanced', form.Flag, 'filter_a',
+                       _('Filter IPv4 A records'),
+                       _('Remove IPv4 addresses from the results and only return IPv6 addresses.'));
+               o.optional = true;
 
                s.taboption('advanced', form.Flag, 'localise_queries',
                        _('Localise queries'),
-                       _('Localise hostname depending on the requesting subnet if multiple IPs are available'));
+                       _('Return answers to DNS queries matching the subnet from which the query was received if multiple IPs are available.'));
 
                if (L.hasSystemFeature('dnsmasq', 'dnssec')) {
                        o = s.taboption('advanced', form.Flag, 'dnssec',
-                               _('DNSSEC'));
+                               _('DNSSEC'),
+                               _('Validate DNS replies and cache DNSSEC data, requires upstream to support DNSSEC.'));
                        o.optional = true;
 
                        o = s.taboption('advanced', form.Flag, 'dnsseccheckunsigned',
                                _('DNSSEC check unsigned'),
-                               _('Requires upstream supports DNSSEC; verify unsigned domain responses really come from unsigned domains'));
+                               _('Verify unsigned domain responses really come from unsigned domains.'));
                        o.default = o.enabled;
                        o.optional = true;
                }
 
-               s.taboption('general', form.Value, 'local',
-                       _('Local server'),
-                       _('Local domain specification. Names matching this domain are never forwarded and are resolved from DHCP or hosts files only'));
-
-               s.taboption('general', form.Value, 'domain',
-                       _('Local domain'),
-                       _('Local domain suffix appended to DHCP names and hosts file entries'));
-
                s.taboption('advanced', form.Flag, 'expandhosts',
                        _('Expand hosts'),
-                       _('Add local domain suffix to names served from hosts files'));
+                       _('Add local domain suffix to names served from hosts files.'));
 
                s.taboption('advanced', form.Flag, 'nonegcache',
                        _('No negative cache'),
-                       _('Do not cache negative replies, e.g. for not existing domains'));
+                       _('Do not cache negative replies, e.g. for non-existent domains.'));
 
-               s.taboption('advanced', form.Value, 'serversfile',
+               o = s.taboption('advanced', form.Value, 'serversfile',
                        _('Additional servers file'),
-                       _('This file may contain lines like \'server=/domain/1.2.3.4\' or \'server=1.2.3.4\' for domain-specific or full upstream <abbr title="Domain Name System">DNS</abbr> servers.'));
+                       _('File listing upstream resolvers, optionally domain-specific, e.g. <code>server=1.2.3.4</code>, <code>server=/domain/1.2.3.4</code>.'));
+               o.placeholder = '/etc/dnsmasq.servers';
 
-               s.taboption('advanced', form.Flag, 'strictorder',
+               o = s.taboption('advanced', form.Flag, 'strictorder',
                        _('Strict order'),
-                       _('<abbr title="Domain Name System">DNS</abbr> servers will be queried in the order of the resolvfile')).optional = true;
-
-               s.taboption('advanced', form.Flag, 'allservers',
-                       _('All Servers'),
-                       _('Query all available upstream <abbr title="Domain Name System">DNS</abbr> servers')).optional = true;
-
-               o = s.taboption('advanced', form.DynamicList, 'bogusnxdomain', _('Bogus NX Domain Override'),
-                       _('List of hosts that supply bogus NX domain results'));
-
+                       _('Upstream resolvers will be queried in the order of the resolv file.'));
                o.optional = true;
-               o.placeholder = '67.215.65.132';
-
-
-               s.taboption('general', form.Flag, 'logqueries',
-                       _('Log queries'),
-                       _('Write received DNS requests to syslog')).optional = true;
-
-               o = s.taboption('general', form.DynamicList, 'server', _('DNS forwardings'),
-                       _('List of <abbr title="Domain Name System">DNS</abbr> servers to forward requests to'));
 
+               o = s.taboption('advanced', form.Flag, 'allservers',
+                       _('All servers'),
+                       _('Query all available upstream resolvers.'));
                o.optional = true;
-               o.placeholder = '/example.org/10.1.2.3';
-               o.validate = validateServerSpec;
-
-
-               o = s.taboption('general', form.DynamicList, 'address', _('Addresses'),
-                       _('List of domains to force to an IP address.'));
 
+               o = s.taboption('advanced', form.DynamicList, 'bogusnxdomain',
+                       _('IPs to override with NXDOMAIN'),
+                       _('List of IP addresses to convert into NXDOMAIN responses.'));
                o.optional = true;
-               o.placeholder = '/router.local/192.168.0.1';
-
-
-               o = s.taboption('general', form.Flag, 'rebind_protection',
-                       _('Rebind protection'),
-                       _('Discard upstream RFC1918 responses'));
-
-               o.rmempty = false;
-
-
-               o = s.taboption('general', form.Flag, 'rebind_localhost',
-                       _('Allow localhost'),
-                       _('Allow upstream responses in the 127.0.0.0/8 range, e.g. for RBL services'));
-
-               o.depends('rebind_protection', '1');
-
-
-               o = s.taboption('general', form.DynamicList, 'rebind_domain',
-                       _('Domain whitelist'),
-                       _('List of domains to allow RFC1918 responses for'));
-               o.optional = true;
-
-               o.depends('rebind_protection', '1');
-               o.placeholder = 'ihost.netflix.com';
-               o.validate = validateAddressList;
-
+               o.placeholder = '64.94.110.11';
 
                o = s.taboption('advanced', form.Value, 'port',
-                       _('<abbr title="Domain Name System">DNS</abbr> server port'),
-                       _('Listening port for inbound DNS queries'));
-
+                       _('DNS server port'),
+                       _('Listening port for inbound DNS queries.'));
                o.optional = true;
                o.datatype = 'port';
                o.placeholder = 53;
 
-
                o = s.taboption('advanced', form.Value, 'queryport',
-                       _('<abbr title="Domain Name System">DNS</abbr> query port'),
-                       _('Fixed source port for outbound DNS queries'));
-
+                       _('DNS query port'),
+                       _('Fixed source port for outbound DNS queries.'));
                o.optional = true;
                o.datatype = 'port';
                o.placeholder = _('any');
 
-
                o = s.taboption('advanced', form.Value, 'dhcpleasemax',
-                       _('<abbr title="maximal">Max.</abbr> <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> leases'),
-                       _('Maximum allowed number of active DHCP leases'));
-
+                       _('Max. DHCP leases'),
+                       _('Maximum allowed number of active DHCP leases.'));
                o.optional = true;
                o.datatype = 'uinteger';
                o.placeholder = _('unlimited');
 
-
                o = s.taboption('advanced', form.Value, 'ednspacket_max',
-                       _('<abbr title="maximal">Max.</abbr> <abbr title="Extension Mechanisms for Domain Name System">EDNS0</abbr> packet size'),
-                       _('Maximum allowed size of EDNS.0 UDP packets'));
-
+                       _('Max. EDNS0 packet size'),
+                       _('Maximum allowed size of EDNS0 UDP packets.'));
                o.optional = true;
                o.datatype = 'uinteger';
                o.placeholder = 1280;
 
-
                o = s.taboption('advanced', form.Value, 'dnsforwardmax',
-                       _('<abbr title="maximal">Max.</abbr> concurrent queries'),
-                       _('Maximum allowed number of concurrent DNS queries'));
-
+                       _('Max. concurrent queries'),
+                       _('Maximum allowed number of concurrent DNS queries.'));
                o.optional = true;
                o.datatype = 'uinteger';
                o.placeholder = 150;
 
                o = s.taboption('advanced', form.Value, 'cachesize',
                        _('Size of DNS query cache'),
-                       _('Number of cached DNS entries (max is 10000, 0 is no caching)'));
+                       _('Number of cached DNS entries, 10000 is maximum, 0 is no caching.'));
                o.optional = true;
                o.datatype = 'range(0,10000)';
-               o.placeholder = 150;
+               o.placeholder = 1000;
 
-               s.taboption('tftp', form.Flag, 'enable_tftp',
-                       _('Enable TFTP server')).optional = true;
+               o = s.taboption('pxe_tftp', form.Flag, 'enable_tftp',
+                       _('Enable TFTP server'),
+                       _('Enable the built-in single-instance TFTP server.'));
+               o.optional = true;
 
-               o = s.taboption('tftp', form.Value, 'tftp_root',
+               o = s.taboption('pxe_tftp', form.Value, 'tftp_root',
                        _('TFTP server root'),
-                       _('Root directory for files served via TFTP'));
-
-               o.optional = true;
+                       _('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>.'));
                o.depends('enable_tftp', '1');
+               o.optional = true;
                o.placeholder = '/';
 
-
-               o = s.taboption('tftp', form.Value, 'dhcp_boot',
+               o = s.taboption('pxe_tftp', form.Value, 'dhcp_boot',
                        _('Network boot image'),
-                       _('Filename of the boot image advertised to clients'));
-
-               o.optional = true;
+                       _('Filename of the boot image advertised to clients.'));
                o.depends('enable_tftp', '1');
+               o.optional = true;
                o.placeholder = 'pxelinux.0';
 
-               o = s.taboption('general', form.Flag, 'localservice',
-                       _('Local Service Only'),
-                       _('Limit DNS service to subnets interfaces on which we are serving DNS.'));
-               o.optional = false;
-               o.rmempty = false;
+               /* PXE - https://openwrt.org/docs/guide-user/base-system/dhcp#booting_options */
+               o = s.taboption('pxe_tftp', form.SectionValue, '__pxe__', form.GridSection, 'boot', null,
+                       _('Special <abbr title="Preboot eXecution Environment">PXE</abbr> boot options for Dnsmasq.'));
+               ss = o.subsection;
+               ss.addremove = true;
+               ss.anonymous = true;
+               ss.nodescriptions = true;
+
+               so = ss.option(form.Value, 'filename',
+                       _('Filename'),
+                       _('Host requests this filename from the boot server.'));
+               so.optional = false;
+               so.placeholder = 'pxelinux.0';
+
+               so = ss.option(form.Value, 'servername',
+                       _('Server name'),
+                       _('The hostname of the boot server'));
+               so.optional = false;
+               so.placeholder = 'myNAS';
+
+               so = ss.option(form.Value, 'serveraddress',
+                       _('Server address'),
+                       _('The IP address of the boot server'));
+               so.optional = false;
+               so.placeholder = '192.168.1.2';
+
+               so = ss.option(form.DynamicList, 'dhcp_option',
+                       _('DHCP Options'),
+                       _('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".'));
+               so.optional = true;
+               so.placeholder = '42,192.168.1.4';
+
+               so = ss.option(widgets.DeviceSelect, 'networkid',
+                       _('Network-ID'),
+                       _('Apply DHCP Options to this net. (Empty = all clients).'));
+               so.optional = true;
+               so.noaliases = true;
+
+               so = ss.option(form.Flag, 'force',
+                       _('Force'),
+                       _('Always send DHCP Options. Sometimes needed, with e.g. PXELinux.'));
+               so.optional = true;
+
+               so = ss.option(form.Value, 'instance',
+                       _('Instance'),
+                       _('Dnsmasq instance to which this boot section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
+               so.optional = true;
+
+               Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
+                       so.value(index, '%s (Domain: %s, Local: %s)'.format(index, val.domain || '?', val.local || '?'));
+               });
 
-               o = s.taboption('general', form.Flag, 'nonwildcard',
-                       _('Non-wildcard'),
-                       _('Bind dynamically to interfaces rather than wildcard address (recommended as linux default)'));
-               o.default = o.enabled;
-               o.optional = false;
-               o.rmempty = true;
+               o = s.taboption('srvhosts', form.SectionValue, '__srvhosts__', form.TableSection, 'srvhost', null,
+                       _('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')
+                       + '<br />' + _('_service: _sip, _ldap, _imap, _stun, _xmpp-client, … . (Note: while _http is possible, no browsers support SRV records.)')
+                       + '<br />' + _('_proto: _tcp, _udp, _sctp, _quic, … .')
+                       + '<br />' + _('You may add multiple records for the same Target.')
+                       + '<br />' + _('Larger weights (of the same prio) are given a proportionately higher probability of being selected.'));
 
-               o = s.taboption('general', form.DynamicList, 'interface',
-                       _('Listen Interfaces'),
-                       _('Limit listening to these interfaces, and loopback.'));
-               o.optional = true;
+               ss = o.subsection;
 
-               o = s.taboption('general', form.DynamicList, 'notinterface',
-                       _('Exclude interfaces'),
-                       _('Prevent listening on these interfaces.'));
-               o.optional = true;
+               ss.addremove = true;
+               ss.anonymous = true;
+               ss.sortable  = true;
+               ss.rowcolors = true;
 
-               o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
-                       _('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 />' +
-                       _('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.'));
+               so = ss.option(form.Value, 'srv', _('SRV'), _('Syntax: <code>_service._proto.example.com</code>.'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+               so.placeholder = '_sip._tcp.example.com';
+
+               so = ss.option(form.Value, 'target', _('Target'), _('CNAME or fqdn'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+               so.placeholder = 'sip.example.com';
+
+               so = ss.option(form.Value, 'port', _('Port'));
+               so.rmempty = false;
+               so.datatype = 'port';
+               so.placeholder = '5060';
+
+               so = ss.option(form.Value, 'class', _('Priority'), _('Ordinal: lower comes first.'));
+               so.rmempty = true;
+               so.datatype = 'range(0,65535)';
+               so.placeholder = '10';
+
+               so = ss.option(form.Value, 'weight', _('Weight'));
+               so.rmempty = true;
+               so.datatype = 'range(0,65535)';
+               so.placeholder = '50';
+
+               o = s.taboption('mxhosts', form.SectionValue, '__mxhosts__', form.TableSection, 'mxhost', null,
+                       _('Bind service records to a domain name: specify the location of services.')
+                        + '<br />' + _('You may add multiple records for the same domain.'));
+
+               ss = o.subsection;
+
+               ss.addremove = true;
+               ss.anonymous = true;
+               ss.sortable  = true;
+               ss.rowcolors = true;
+               ss.nodescriptions = true;
+
+               so = ss.option(form.Value, 'domain', _('Domain'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+               so.placeholder = 'example.com';
+
+               so = ss.option(form.Value, 'relay', _('Relay'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+               so.placeholder = 'relay.example.com';
+
+               so = ss.option(form.Value, 'pref', _('Priority'), _('Ordinal: lower comes first.'));
+               so.rmempty = true;
+               so.datatype = 'range(0,65535)';
+               so.placeholder = '0';
+
+               o = s.taboption('cnamehosts', form.SectionValue, '__cname__', form.TableSection, 'cname', null, 
+                       _('Set an alias for a hostname.'));
 
                ss = o.subsection;
 
                ss.addremove = true;
                ss.anonymous = true;
+               ss.sortable  = true;
+               ss.rowcolors = true;
+               ss.nodescriptions = true;
+
+               so = ss.option(form.Value, 'cname', _('Domain'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+               so.placeholder = 'www.example.com';
+
+               so = ss.option(form.Value, 'target', _('Target'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+               so.placeholder = 'example.com';
+
+               o = s.taboption('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
+                       _('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.'));
+
+               ss = o.subsection;
+
+               ss.addremove = true;
+               ss.anonymous = true;
+               ss.sortable  = true;
 
                so = ss.option(form.Value, 'name', _('Hostname'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+
+               so = ss.option(form.Value, 'ip', _('IP address'));
+               so.rmempty = false;
+               so.datatype = 'ipaddr';
+
+               var ipaddrs = {};
+
+               Object.keys(hosts).forEach(function(mac) {
+                       var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4);
+
+                       for (var i = 0; i < addrs.length; i++)
+                               ipaddrs[addrs[i]] = hosts[mac].name || mac;
+               });
+
+               L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
+                       so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
+               });
+
+               o = s.taboption('ipsets', form.SectionValue, '__ipsets__', form.GridSection, 'ipset', null,
+                       _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.'));
+
+               ss = o.subsection;
+
+               ss.addremove = true;
+               ss.anonymous = true;
+               ss.sortable  = true;
+
+               so = ss.option(form.DynamicList, 'name', _('IP set'));
+               so.rmempty = false;
+               so.datatype = 'string';
+
+               so = ss.option(form.DynamicList, 'domain', _('Domain'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+
+               o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
+                       _('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 />' +
+                       _('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 />' +
+                       _('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).'));
+
+               ss = o.subsection;
+
+               ss.addremove = true;
+               ss.anonymous = true;
+               ss.sortable = true;
+               ss.nodescriptions = true;
+               ss.max_cols = 8;
+               ss.modaltitle = _('Edit static lease');
+
+               so = ss.option(form.Value, 'name', 
+                       _('Hostname'),
+                       _('Optional hostname to assign'));
                so.validate = validateHostname;
                so.rmempty  = true;
                so.write = function(section, value) {
@@ -517,20 +792,35 @@ return view.extend({
                        uci.unset('dhcp', section, 'dns');
                };
 
-               so = ss.option(form.Value, 'mac', _('<abbr title="Media Access Control">MAC</abbr>-Address'));
-               so.datatype = 'list(macaddr)';
+               so = ss.option(form.Value, 'mac',
+                       _('MAC address(es)'),
+                       _('The hardware address(es) of this entry/host, separated by spaces.') + '<br /><br />' + 
+                       _('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.'));
+               //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
+               so.validate = function(section_id, value) {
+                       var macaddrs = L.toArray(value);
+
+                       for (var i = 0; i < macaddrs.length; i++)
+                               if (!macaddrs[i].match(/^([a-fA-F0-9]{2}|\*):([a-fA-F0-9]{2}:|\*:){4}(?:[a-fA-F0-9]{2}|\*)$/))
+                                       return _('Expecting a valid MAC address, optionally including wildcards');
+
+                       return true;
+               };
                so.rmempty  = true;
                so.cfgvalue = function(section) {
                        var macs = L.toArray(uci.get('dhcp', section, 'mac')),
                            result = [];
 
                        for (var i = 0, mac; (mac = macs[i]) != null; i++)
-                               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))
-                                       result.push('%02X:%02X:%02X:%02X:%02X:%02X'.format(
+                               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)) {
+                                       var m = [
                                                parseInt(RegExp.$1, 16), parseInt(RegExp.$2, 16),
                                                parseInt(RegExp.$3, 16), parseInt(RegExp.$4, 16),
-                                               parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16)));
+                                               parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16)
+                                       ];
 
+                                       result.push(m.map(function(n) { return isNaN(n) ? '*' : '%02X'.format(n) }).join(':'));
+                               }
                        return result.length ? result.join(' ') : null;
                };
                so.renderWidget = function(section_id, option_index, cfgvalue) {
@@ -563,14 +853,15 @@ return view.extend({
                        so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
                });
 
-               so = ss.option(form.Value, 'ip', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Address'));
+               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.'));
+               so.value('ignore', _('Ignore'));
                so.datatype = 'or(ip4addr,"ignore")';
                so.validate = function(section, value) {
                        var m = this.section.formvalue(section, 'mac'),
                            n = this.section.formvalue(section, 'name');
 
                        if ((m == null || m == '') && (n == null || n == ''))
-                               return _('One of hostname or mac address must be specified!');
+                               return _('One of hostname or MAC address must be specified!');
 
                        if (value == null || value == '' || value == 'ignore')
                                return true;
@@ -581,7 +872,6 @@ return view.extend({
                                if (leases[i]['.name'] != section && leases[i].ip == value)
                                        return _('The IP address %h is already used by another static lease').format(value);
 
-
                        for (var i = 0; i < pools.length; i++) {
                                var net_mask = calculateNetwork(value, pools[i].netmask);
 
@@ -592,29 +882,64 @@ return view.extend({
                        return _('The IP address is outside of any DHCP pool address range');
                };
 
-               var ipaddrs = {};
-
-               Object.keys(hosts).forEach(function(mac) {
-                       var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4);
-
-                       for (var i = 0; i < addrs.length; i++)
-                               ipaddrs[addrs[i]] = hosts[mac].name;
-               });
-
                L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
                        so.value(ipv4, ipaddrs[ipv4] ? '%s (%s)'.format(ipv4, ipaddrs[ipv4]) : ipv4);
                });
 
-               so = ss.option(form.Value, 'leasetime', _('Lease time'));
+               so = ss.option(form.Value, 'leasetime', 
+                       _('Lease time'),
+                       _('Host-specific lease time, e.g. <code>5m</code>, <code>3h</code>, <code>7d</code>.'));
                so.rmempty = true;
-
-               so = ss.option(form.Value, 'duid', _('<abbr title="The DHCP Unique Identifier">DUID</abbr>'));
+               so.value('5m', _('5m (5 minutes)'));
+               so.value('3h', _('3h (3 hours)'));
+               so.value('12h', _('12h (12 hours - default)'));
+               so.value('7d', _('7d (7 days)'));
+               so.value('infinite', _('infinite (lease does not expire)'));
+
+               so = ss.option(form.Value, 'duid',
+                       _('DUID'),
+                       _('The DHCPv6-DUID (DHCP unique identifier) of this host.'));
                so.datatype = 'and(rangelength(20,36),hexstring)';
                Object.keys(duids).forEach(function(duid) {
                        so.value(duid, '%s (%s)'.format(duid, duids[duid].hostname || duids[duid].macaddr || duids[duid].ip6addr || '?'));
                });
 
-               so = ss.option(form.Value, 'hostid', _('<abbr title="Internet Protocol Version 6">IPv6</abbr>-Suffix (hex)'));
+               so = ss.option(form.Value, 'hostid',
+                       _('IPv6-Suffix (hex)'),
+                       _('The IPv6 interface identifier (address suffix) as hexadecimal number (max. 8 chars).'));
+               so.datatype = 'and(rangelength(0,8),hexstring)';
+
+               so = ss.option(form.DynamicList, 'tag',
+                       _('Tag'),
+                       _('Assign new, freeform tags to this entry.'));
+
+               so = ss.option(form.DynamicList, 'match_tag',
+                       _('Match Tag'),
+                       _('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 />' +
+                       _('Ignore requests from unknown machines using <em>!known</em>.') + '<br /><br />' +
+                       _('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.'));
+               so.value('known', _('known'));
+               so.value('!known', _('!known (not known)'));
+               so.value('known-othernet', _('known-othernet (on different subnet)'));
+               so.optional = true;
+
+               so = ss.option(form.Value, 'instance',
+                       _('Instance'),
+                       _('Dnsmasq instance to which this DHCP host section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
+               so.optional = true;
+
+               Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
+                       so.value(index, '%s (Domain: %s, Local: %s)'.format(index, val.domain || '?', val.local || '?'));
+               });
+
+
+               so = ss.option(form.Flag, 'broadcast',
+                       _('Broadcast'),
+                       _('Force broadcast DHCP response.'));
+
+               so = ss.option(form.Flag, 'dns',
+                       _('Forward/reverse DNS'),
+                       _('Add static forward and reverse DNS entries for this host.'));
 
                o = s.taboption('leases', CBILeaseStatus, '__status__');
 
@@ -638,8 +963,17 @@ return view.extend({
                                                        else
                                                                exp = '%t'.format(lease.expires);
 
+                                                       var hint = lease.macaddr ? hosts[lease.macaddr] : null,
+                                                           name = hint ? hint.name : null,
+                                                           host = null;
+
+                                                       if (name && lease.hostname && lease.hostname != name)
+                                                               host = '%s (%s)'.format(lease.hostname, name);
+                                                       else if (lease.hostname)
+                                                               host = lease.hostname;
+
                                                        return [
-                                                               lease.hostname || '?',
+                                                               host || '-',
                                                                lease.ipaddr,
                                                                lease.macaddr,
                                                                exp