luci-mod-network: rework DHCP relay settings
[project/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / view / network / dhcp.js
index e193111c487e847565d9a3321e90f94646db1f9a..04b57a277de68f7f835f9cef60a15c288cb61dba 100644 (file)
@@ -126,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))
@@ -156,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;
@@ -231,7 +233,8 @@ return view.extend({
                return Promise.all([
                        callHostHints(),
                        callDUIDHints(),
-                       getDHCPPools()
+                       getDHCPPools(),
+                       network.getNetworks()
                ]);
        },
 
@@ -240,6 +243,7 @@ return view.extend({
                    hosts = hosts_duids_pools[0],
                    duids = hosts_duids_pools[1],
                    pools = hosts_duids_pools[2],
+                   networks = hosts_duids_pools[3],
                    m, s, o, ss, so;
 
                m = new form.Map('dhcp', _('DHCP and DNS'),
@@ -250,14 +254,16 @@ return view.extend({
                s.addremove = false;
 
                s.tab('general', _('General Settings'));
-               s.tab('files', _('Resolv and Hosts Files'));
-               s.tab('pxe_tftp', _('PXE/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('ipsets', _('IP Sets'));
+               s.tab('cnamehosts', _('CNAME'));
+               s.tab('pxe_tftp', _('PXE/TFTP Settings'));
 
                s.taboption('general', form.Flag, 'domainneeded',
                        _('Domain required'),
@@ -298,7 +304,7 @@ return view.extend({
 
                o = s.taboption('general', form.DynamicList, 'ipset',
                        _('IP sets'),
-                       _('List of IP sets to populate with the specified domain IPs.'));
+                       _('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';
 
@@ -345,6 +351,70 @@ return view.extend({
                o.optional = true;
                o.placeholder = 'loopback';
 
+               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, 'local_addr', _('Relay from'));
+               so.rmempty = false;
+               so.datatype = 'ipaddr';
+
+               for (var family = 4; family <= 6; family += 2) {
+                       for (var i = 0; i < networks.length; i++) {
+                               if (networks[i].getName() != 'loopback') {
+                                       var addrs = (family == 6) ? networks[i].getIP6Addrs() : networks[i].getIPAddrs();
+                                       for (var j = 0; j < addrs.length; j++) {
+                                               var addr = addrs[j].split('/')[0];
+                                               so.value(addr, E([], [
+                                                       addr, ' (',
+                                                       widgets.NetworkSelect.prototype.renderIfaceBadge(networks[i]),
+                                                       ')'
+                                               ]));
+                                       }
+                               }
+                       }
+               }
+
+               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 "Relay from" and "Relay to address" must be specified.');
+
+                       if ((validation.parseIPv6(m) && validation.parseIPv6(n)) ||
+                               validation.parseIPv4(m) && validation.parseIPv4(n))
+                               return true;
+                       else
+                               return _('Address families of "Relay from" and "Relay to address" must match.')
+               };
+
+               so = ss.option(widgets.NetworkSelect, 'interface', _('Only accept replies via'));
+               so.optional = true;
+               so.rmempty = false;
+               so.placeholder = 'lan';
+
                s.taboption('files', form.Flag, 'readethers',
                        _('Use <code>/etc/ethers</code>'),
                        _('Read <code>/etc/ethers</code> to configure the DHCP server.'));
@@ -389,10 +459,21 @@ return view.extend({
                o.default = o.enabled;
 
                s.taboption('advanced', form.Flag, 'filterwin2k',
-                       _('Filter useless'),
-                       _('Avoid uselessly triggering dial-on-demand links (filters SRV/SOA records and names with underscores).') + '<br />' +
+                       _('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'),
                        _('Return answers to DNS queries matching the subnet from which the query was received if multiple IPs are available.'));
@@ -479,7 +560,7 @@ return view.extend({
                        _('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;
 
                o = s.taboption('pxe_tftp', form.Flag, 'enable_tftp',
                        _('Enable TFTP server'),
@@ -618,6 +699,27 @@ return view.extend({
                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.'));
 
@@ -649,7 +751,7 @@ return view.extend({
                });
 
                o = s.taboption('ipsets', form.SectionValue, '__ipsets__', form.GridSection, 'ipset', null,
-                       _('List of IP sets to populate with the specified domain IPs.'));
+                       _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.'));
 
                ss = o.subsection;
 
@@ -666,16 +768,22 @@ return view.extend({
                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 />' +
-                       _('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.'));
+                       _('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'));
+               so = ss.option(form.Value, 'name', 
+                       _('Hostname'),
+                       _('Optional hostname to assign'));
                so.validate = validateHostname;
                so.rmempty  = true;
                so.write = function(section, value) {
@@ -687,20 +795,35 @@ return view.extend({
                        uci.unset('dhcp', section, 'dns');
                };
 
-               so = ss.option(form.Value, 'mac', _('MAC 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) {
@@ -733,7 +856,8 @@ return view.extend({
                        so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
                });
 
-               so = ss.option(form.Value, 'ip', _('IPv4 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'),
@@ -765,16 +889,60 @@ return view.extend({
                        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', _('DUID'));
+               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', _('IPv6 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__');