];
}
+function generateDnsmasqInstanceEntry(data) {
+ const nameValueMap = new Map(Object.entries(data));
+ let formatString = nameValueMap.get('.index') + ' (' + _('Name') + (nameValueMap.get('.anonymous') ? ': dnsmasq[' + nameValueMap.get('.index') + ']': ': ' + nameValueMap.get('.name'));
+
+ if (data.domain) {
+ formatString += ', ' + _('Domain') + ': ' + data.domain;
+ }
+ if (data.local) {
+ formatString += ', ' + _('Local') + ': ' + data.local;
+ }
+ formatString += ')';
+
+ return nameValueMap.get('.name'), formatString;
+}
+
function getDHCPPools() {
return uci.load('dhcp').then(function() {
let sections = uci.sections('dhcp', 'dhcp'),
return true;
}
+function expandAndFormatMAC(macs) {
+ let result = [];
+
+ macs.forEach(mac => {
+ if (isValidMAC(mac)) {
+ const expandedMac = mac.split(':').map(part => {
+ return (part.length === 1 && part !== '*') ? '0' + part : part;
+ }).join(':').toUpperCase();
+ result.push(expandedMac);
+ }
+ });
+
+ return result.length ? result.join(' ') : null;
+}
+
+function isValidMAC(sid, s) {
+ if (!s)
+ return true;
+
+ let macaddrs = L.toArray(s);
+
+ for (var i = 0; i < macaddrs.length; i++)
+ if (!macaddrs[i].match(/^(([0-9a-f]{1,2}|\*)[:-]){5}([0-9a-f]{1,2}|\*)$/i))
+ return _('Expecting a valid MAC address, optionally including wildcards') + _('; invalid MAC: ') + macaddrs[i];
+
+ return true;
+}
+
function validateMACAddr(pools, sid, s) {
if (s == null || s == '')
return true;
}
}
- return true;
+ return isValidMAC(sid, s);
}
return view.extend({
callHostHints(),
callDUIDHints(),
getDHCPPools(),
- network.getDevices()
+ network.getNetworks()
]);
},
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;
+ networks = hosts_duids_pools[3],
+ m, s, o, ss, so, dnss;
+
+ let noi18nstrings = {
+ etc_hosts: '<code>/etc/hosts</code>',
+ etc_ethers: '<code>/etc/ethers</code>',
+ localhost_v6: '<code>::1</code>',
+ loopback_slash_8_v4: '<code>127.0.0.0/8</code>',
+ not_found: '<code>Not found</code>',
+ nxdomain: '<code>NXDOMAIN</code>',
+ rfc_1918_link: '<a href="https://www.rfc-editor.org/rfc/rfc1918">RFC1918</a>',
+ rfc_4193_link: '<a href="https://www.rfc-editor.org/rfc/rfc4193">RFC4193</a>',
+ rfc_4291_link: '<a href="https://www.rfc-editor.org/rfc/rfc4291">RFC4291</a>',
+ rfc_6303_link: '<a href="https://www.rfc-editor.org/rfc/rfc6303">RFC6303</a>',
+ reverse_arpa: '<code>*.IN-ADDR.ARPA,*.IP6.ARPA</code>',
+ servers_file_entry01: '<code>server=1.2.3.4</code>',
+ servers_file_entry02: '<code>server=/domain/1.2.3.4</code>',
+
+ };
+
+ function customi18n(template, values) {
+ if (!values)
+ values = noi18nstrings;
+ return template.replace(/\{(\w+)\}/g, (match, key) => values[key] || match);
+ };
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');
- s.anonymous = true;
- s.addremove = false;
+ s.anonymous = false;
+ s.addremove = true;
+ s.addbtntitle = _('Add server instance', 'Dnsmasq instance');
+
+ s.renderContents = function(/* ... */) {
+ var renderTask = form.TypedSection.prototype.renderContents.apply(this, arguments),
+ sections = this.cfgsections();
+
+ return Promise.resolve(renderTask).then(function(nodes) {
+ if (sections.length < 2) {
+ nodes.querySelector('#cbi-dhcp-dnsmasq > h3').remove();
+ nodes.querySelector('#cbi-dhcp-dnsmasq > .cbi-section-remove').remove();
+ }
+ else {
+ nodes.querySelectorAll('#cbi-dhcp-dnsmasq > .cbi-section-remove').forEach(function(div, i) {
+ var section = uci.get('dhcp', sections[i]),
+ hline = div.nextElementSibling,
+ btn = div.firstElementChild;
+
+ if (!section || section['.anonymous']) {
+ hline.innerText = i ? _('Unnamed instance #%d', 'Dnsmasq instance').format(i+1) : _('Default instance', 'Dnsmasq instance');
+ btn.innerText = i ? _('Remove instance #%d', 'Dnsmasq instance').format(i+1) : _('Remove default instance', 'Dnsmasq instance');
+ }
+ else {
+ hline.innerText = _('Instance "%q"', 'Dnsmasq instance').format(section['.name']);
+ btn.innerText = _('Remove instance "%q"', 'Dnsmasq instance').format(section['.name']);
+ }
+ });
+ }
- s.tab('general', _('General Settings'));
- s.tab('relay', _('Relay'));
- s.tab('files', _('Resolv and Hosts Files'));
- s.tab('pxe_tftp', _('PXE/TFTP Settings'));
- s.tab('advanced', _('Advanced Settings'));
+ nodes.querySelector('#cbi-dhcp-dnsmasq > .cbi-section-create input').placeholder = _('New instance name…', 'Dnsmasq instance');
+
+ return nodes;
+ });
+ };
+
+
+ s.tab('general', _('General'));
+ s.tab('devices', _('Devices & Ports'));
+ s.tab('dnsrecords', _('DNS Records'));
+ s.tab('dnssecopt', _('DNSSEC'));
+ s.tab('filteropts', _('Filter'));
+ s.tab('forward', _('Forwards'));
+ s.tab('limits', _('Limits'));
+ s.tab('logging', _('Log'));
+ s.tab('files', _('Resolv & Hosts Files'));
s.tab('leases', _('Static Leases'));
- s.tab('hosts', _('Hostnames'));
- s.tab('srvhosts', _('SRV'));
- s.tab('mxhosts', _('MX'));
s.tab('ipsets', _('IP Sets'));
+ s.tab('relay', _('Relay'));
+ s.tab('pxe_tftp', _('PXE/TFTP'));
- s.taboption('general', form.Flag, 'domainneeded',
+ s.taboption('filteropts', form.Flag, 'domainneeded',
_('Domain required'),
- _('Do not forward DNS queries without dots or domain parts.'));
-
+ _('Never forward DNS queries which lack dots or domain parts.') + '<br />' +
+ customi18n(_('Names not in {etc_hosts} are answered {not_found}.') )
+ );
s.taboption('general', form.Flag, 'authoritative',
_('Authoritative'),
_('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.'));
+ o = s.taboption('general', form.Value, 'local',
+ _('Resolve these locally'),
+ _('Never forward these matching domains or subdomains; resolve from DHCP or hosts files only.'));
+ o.placeholder = '/internal.example.com/private.example.com/example.org';
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',
+ s.taboption('general', form.Flag, 'expandhosts',
+ _('Expand hosts'),
+ _('Add local domain suffix to names served from hosts files.'));
+
+ o = s.taboption('logging', form.Flag, 'logqueries',
_('Log queries'),
- _('Write received DNS queries to syslog.'));
+ _('Write received DNS queries to syslog.') + ' ' + _('Dump cache on SIGUSR1, include requesting IP.'));
+ o.optional = true;
+
+ o = s.taboption('logging', form.Flag, 'logdhcp',
+ _('Extra DHCP logging'),
+ _('Log all options sent to DHCP clients and the tags used to determine them.'));
o.optional = true;
- o = s.taboption('general', form.DynamicList, 'server',
- _('DNS forwardings'),
- _('List of upstream resolvers to forward queries to.'));
+ o = s.taboption('logging', form.Value, 'logfacility',
+ _('Log facility'),
+ _('Set log class/facility for syslog entries.'));
+ o.optional = true;
+ o.value('KERN');
+ o.value('USER');
+ o.value('MAIL');
+ o.value('DAEMON');
+ o.value('AUTH');
+ o.value('LPR');
+ o.value('NEWS');
+ o.value('UUCP');
+ o.value('CRON');
+ o.value('LOCAL0');
+ o.value('LOCAL1');
+ o.value('LOCAL2');
+ o.value('LOCAL3');
+ o.value('LOCAL4');
+ o.value('LOCAL5');
+ o.value('LOCAL6');
+ o.value('LOCAL7');
+ o.value('-', _('stderr'));
+
+ o = s.taboption('forward', form.DynamicList, 'server',
+ _('DNS Forwards'),
+ _('Forward specific domain queries to specific upstream servers.'));
o.optional = true;
- o.placeholder = '/example.org/10.1.2.3';
+ 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.'));
+ customi18n(_('Syntax: {code_syntax}.'),
+ {code_syntax: '<code>/fqdn[/fqdn…]/[ipaddr]</code>'}) + '<br />' +
+ customi18n(_('{example_nx} returns {nxdomain}.',
+ 'hint: <code>/example.com/</code> returns <code>NXDOMAIN</code>.'),
+ {example_nx: '<code>/example.com/</code>', nxdomain: '<code>NXDOMAIN</code>'}) + '<br />' +
+ customi18n(_('{any_domain} matches any domain (and returns {nxdomain}).',
+ 'hint: <code>/#/</code> matches any domain (and returns NXDOMAIN).'),
+ {any_domain:'<code>/#/</code>', nxdomain: '<code>NXDOMAIN</code>'}) + '<br />' +
+ customi18n(
+ _('{example_null} returns {null_addr} addresses ({null_ipv4}, {null_ipv6}) for {example_com} and its subdomains.',
+ 'hint: <code>/example.com/#</code> returns NULL addresses (<code>0.0.0.0</code>, <code>::</code>) for example.com and its subdomains.'),
+ { example_null: '<code>/example.com/#</code>',
+ null_addr: '<code>NULL</code>',
+ null_ipv4: '<code>0.0.0.0</code>',
+ null_ipv6: '<code>::</code>',
+ example_com: '<code>example.com</code>',
+ }
+ )
+ );
o.optional = true;
o.placeholder = '/router.local/router.lan/192.168.0.1';
o.optional = true;
o.placeholder = '/example.org/ipset,ipset6';
- o = s.taboption('general', form.Flag, 'rebind_protection',
+ o = s.taboption('filteropts', form.Flag, 'rebind_protection',
_('Rebind protection'),
- _('Discard upstream responses containing <a href="%s">RFC1918</a> addresses.').format('https://datatracker.ietf.org/doc/html/rfc1918'));
+ customi18n(_('Discard upstream responses containing {rfc_1918_link} addresses.') ) + '<br />' +
+ customi18n(_('Discard also upstream responses containing {rfc_4193_link}, Link-Local and private IPv4-Mapped {rfc_4291_link} IPv6 Addresses.') )
+ );
o.rmempty = false;
- o = s.taboption('general', form.Flag, 'rebind_localhost',
+ o = s.taboption('filteropts', 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.'));
+ customi18n(
+ _('Exempt {loopback_slash_8_v4} and {localhost_v6} from rebinding checks, e.g. for <abbr title="Real-time Block List">RBL</abbr> services.')
+ )
+ );
o.depends('rebind_protection', '1');
- o = s.taboption('general', form.DynamicList, 'rebind_domain',
+ o = s.taboption('filteropts', form.DynamicList, 'rebind_domain',
_('Domain whitelist'),
- _('List of domains to allow RFC1918 responses for.'));
+ customi18n(_('List of domains to allow {rfc_1918_link} 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',
+ o = s.taboption('filteropts', 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',
+ o = s.taboption('devices', form.Flag, 'nonwildcard',
_('Non-wildcard'),
- _('Bind dynamically to interfaces rather than wildcard address.'));
+ _('Bind only to configured interface addresses, instead of the wildcard address.'));
o.default = o.enabled;
o.optional = false;
o.rmempty = true;
- o = s.taboption('general', form.DynamicList, 'interface',
+ o = s.taboption('devices', widgets.NetworkSelect, 'interface',
_('Listen interfaces'),
_('Listen only on the specified interfaces, and loopback if not excluded explicitly.'));
- o.optional = true;
- o.placeholder = 'lan';
+ o.multiple = true;
+ o.nocreate = true;
- o = s.taboption('general', form.DynamicList, 'notinterface',
+ o = s.taboption('devices', widgets.NetworkSelect, 'notinterface',
_('Exclude interfaces'),
_('Do not listen on the specified interfaces.'));
- o.optional = true;
- o.placeholder = 'loopback';
+ 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.')
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 = 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 < 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]);
+ 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 = ss.option(form.Value, 'server_addr', _('Relay to address'));
so.rmempty = false;
so.optional = false;
so.placeholder = '192.168.10.1#535';
var m = this.section.formvalue(section, 'local_addr'),
n = this.section.formvalue(section, 'server_addr'),
p;
- if (n != null && n != '')
- p = n.split('#');
+
+ if (!m || !n) {
+ return _('Both "Relay from" and "Relay to address" must be specified.');
+ }
+ else {
+ 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.')
+ 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.')
+ }
+ return true;
};
+
+ 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.'));
+ customi18n(_('Use {etc_ethers}') ),
+ customi18n(_('Read {etc_ethers} to configure the DHCP server.') )
+ );
s.taboption('files', form.Value, 'leasefile',
_('Lease file'),
o.placeholder = '/tmp/resolv.conf.d/resolv.conf.auto';
o.optional = true;
+ o = s.taboption('files', form.Flag, 'strictorder',
+ _('Strict order'),
+ _('Query upstream resolvers in the order they appear in the resolv file.'));
+ o.optional = true;
+
o = s.taboption('files', form.Flag, 'nohosts',
- _('Ignore <code>/etc/hosts</code>'));
+ customi18n(_('Ignore {etc_hosts}') )
+ );
o.optional = true;
o = s.taboption('files', form.DynamicList, 'addnhosts',
o.optional = true;
o.placeholder = '/etc/dnsmasq.hosts';
- o = s.taboption('advanced', form.Flag, 'quietdhcp',
+ o = s.taboption('logging', form.Flag, 'quietdhcp',
_('Suppress logging'),
_('Suppress logging of the routine operation for the DHCP protocol.'));
o.optional = true;
+ o.depends('logdhcp', '0');
- o = s.taboption('advanced', form.Flag, 'sequential_ip',
+ o = s.taboption('general', form.Flag, 'sequential_ip',
_('Allocate IPs sequentially'),
_('Allocate IP addresses sequentially, starting from the lowest available address.'));
o.optional = true;
- o = s.taboption('advanced', form.Flag, 'boguspriv',
+ o = s.taboption('filteropts', form.Flag, 'boguspriv',
_('Filter private'),
- _('Do not forward reverse lookups for local networks.'));
+ customi18n(
+ _('Reject reverse lookups to {rfc_6303_link} IP ranges ({reverse_arpa}) not in {etc_hosts}.') )
+ );
o.default = o.enabled;
- s.taboption('advanced', form.Flag, 'filterwin2k',
+ s.taboption('filteropts', form.Flag, 'filterwin2k',
_('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',
+ o = s.taboption('filteropts', 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',
+ o = s.taboption('filteropts', 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',
+ s.taboption('filteropts', 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.'));
+ customi18n(_('Limit response records (from {etc_hosts}) to those that fall within the subnet of the querying interface.') ) + '<br />' +
+ _('This prevents unreachable IPs in subnets not accessible to you.') + '<br />' +
+ _('Note: IPv4 only.'));
if (L.hasSystemFeature('dnsmasq', 'dnssec')) {
- o = s.taboption('advanced', form.Flag, 'dnssec',
+ o = s.taboption('dnssecopt', form.Flag, 'dnssec',
_('DNSSEC'),
_('Validate DNS replies and cache DNSSEC data, requires upstream to support DNSSEC.'));
o.optional = true;
- o = s.taboption('advanced', form.Flag, 'dnsseccheckunsigned',
+ o = s.taboption('dnssecopt', form.Flag, 'dnsseccheckunsigned',
_('DNSSEC check unsigned'),
_('Verify unsigned domain responses really come from unsigned domains.'));
o.default = o.enabled;
o.optional = true;
}
- s.taboption('advanced', form.Flag, 'expandhosts',
- _('Expand hosts'),
- _('Add local domain suffix to names served from hosts files.'));
-
- s.taboption('advanced', form.Flag, 'nonegcache',
+ s.taboption('filteropts', form.Flag, 'nonegcache',
_('No negative cache'),
_('Do not cache negative replies, e.g. for non-existent domains.'));
- o = s.taboption('advanced', form.Value, 'serversfile',
+ o = s.taboption('forward', form.Value, 'serversfile',
_('Additional servers file'),
- _('File listing upstream resolvers, optionally domain-specific, e.g. <code>server=1.2.3.4</code>, <code>server=/domain/1.2.3.4</code>.'));
+ customi18n(_('File listing upstream resolvers, optionally domain-specific, e.g. {servers_file_entry01}, {servers_file_entry02}.') )
+ );
o.placeholder = '/etc/dnsmasq.servers';
- o = s.taboption('advanced', form.Flag, 'strictorder',
- _('Strict order'),
- _('Upstream resolvers will be queried in the order of the resolv file.'));
- o.optional = true;
-
- o = s.taboption('advanced', form.Flag, 'allservers',
+ o = s.taboption('general', form.Flag, 'allservers',
_('All servers'),
- _('Query all available upstream resolvers.'));
+ _('Query all available upstream resolvers.') + ' ' + _('First answer wins.'));
o.optional = true;
- o = s.taboption('advanced', form.DynamicList, 'bogusnxdomain',
- _('IPs to override with NXDOMAIN'),
- _('List of IP addresses to convert into NXDOMAIN responses.'));
+ o = s.taboption('filteropts', form.DynamicList, 'bogusnxdomain',
+ customi18n(_('IPs to override with {nxdomain}') ),
+ customi18n(_('Transform replies which contain the specified addresses or subnets into {nxdomain} responses.') )
+ );
o.optional = true;
o.placeholder = '64.94.110.11';
- o = s.taboption('advanced', form.Value, 'port',
+ o = s.taboption('devices', form.Value, 'port',
_('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',
+ o = s.taboption('devices', form.Value, 'queryport',
_('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',
+ o = s.taboption('devices', form.Value, 'minport',
+ _('Minimum source port #'),
+ _('Min valid value %s.').format('<code>1024</code>') + ' ' + _('Useful for systems behind firewalls.'));
+ o.optional = true;
+ o.datatype = 'port';
+ o.placeholder = 1024;
+ o.depends('queryport', '');
+
+ o = s.taboption('devices', form.Value, 'maxport',
+ _('Maximum source port #'),
+ _('Max valid value %s.').format('<code>65535</code>') + ' ' + _('Useful for systems behind firewalls.'));
+ o.optional = true;
+ o.datatype = 'port';
+ o.placeholder = 50000;
+ o.depends('queryport', '');
+
+ o = s.taboption('limits', form.Value, 'dhcpleasemax',
_('Max. DHCP leases'),
_('Maximum allowed number of active DHCP leases.'));
o.optional = true;
o.datatype = 'uinteger';
- o.placeholder = _('unlimited');
+ o.placeholder = 150;
- o = s.taboption('advanced', form.Value, 'ednspacket_max',
+ o = s.taboption('limits', form.Value, 'ednspacket_max',
_('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',
+ o = s.taboption('limits', form.Value, 'dnsforwardmax',
_('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',
+ o = s.taboption('limits', form.Value, 'cachesize',
_('Size of DNS query cache'),
_('Number of cached DNS entries, 10000 is maximum, 0 is no caching.'));
o.optional = true;
o.datatype = 'range(0,10000)';
o.placeholder = 1000;
+ o = s.taboption('limits', form.Value, 'min_cache_ttl',
+ _('Min cache TTL'),
+ _('Extend short TTL values to the seconds value given when caching them. Use with caution.') +
+ _(' (Max 1h == 3600)'));
+ o.optional = true;
+ o.placeholder = 60;
+
+ o = s.taboption('limits', form.Value, 'max_cache_ttl',
+ _('Max cache TTL'),
+ _('Set a maximum seconds TTL value for entries in the cache.'));
+ o.optional = true;
+ o.placeholder = 3600;
+
o = s.taboption('pxe_tftp', form.Flag, 'enable_tftp',
_('Enable TFTP server'),
_('Enable the built-in single-instance TFTP server.'));
ss = o.subsection;
ss.addremove = true;
ss.anonymous = true;
+ ss.modaltitle = _('Edit PXE/TFTP/BOOTP Host');
ss.nodescriptions = true;
so = ss.option(form.Value, 'filename',
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".'));
+ _('Additional options to send to the below match tags.') + '<br />' +
+ _('%s means "the address of the system running dnsmasq".').format('<code>0.0.0.0</code>'));
so.optional = true;
- so.placeholder = '42,192.168.1.4';
+ so.placeholder = 'option:root-path,192.168.1.2:/data/netboot/root';
- so = ss.option(widgets.DeviceSelect, 'networkid',
- _('Network-ID'),
- _('Apply DHCP Options to this net. (Empty = all clients).'));
+ so = ss.option(form.Value, 'networkid',
+ _('Match this Tag'),
+ _('Only DHCP Clients with this tag are sent this boot option.'));
so.optional = true;
so.noaliases = true;
so = ss.option(form.Flag, 'force',
_('Force'),
- _('Always send DHCP Options. Sometimes needed, with e.g. PXELinux.'));
+ _('Always send the chosen DHCP options. Sometimes needed, with e.g. PXELinux.'));
so.optional = true;
so = ss.option(form.Value, 'instance',
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 || '?'));
+ var name, display_str = generateDnsmasqInstanceEntry(val);
+ so.value(index, display_str);
});
- o = s.taboption('srvhosts', form.SectionValue, '__srvhosts__', form.TableSection, 'srvhost', null,
+ o = s.taboption('dnsrecords', form.SectionValue, '__dnsrecords__', form.TypedSection, '__dnsrecords__');
+
+ dnss = o.subsection;
+
+ dnss.anonymous = true;
+ dnss.cfgsections = function() { return [ '__dnsrecords__' ] };
+
+ dnss.tab('hosts', _('Hostnames'));
+ dnss.tab('srvhosts', _('SRV'));
+ dnss.tab('mxhosts', _('MX'));
+ dnss.tab('cnamehosts', _('CNAME'));
+
+ o = dnss.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, … .')
ss.sortable = true;
ss.rowcolors = true;
- so = ss.option(form.Value, 'srv', _('SRV'), _('Syntax: <code>_service._proto.example.com</code>.'));
+ 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.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.placeholder = 'sip.example.com.';
so = ss.option(form.Value, 'port', _('Port'));
so.rmempty = false;
so.datatype = 'range(0,65535)';
so.placeholder = '50';
- o = s.taboption('mxhosts', form.SectionValue, '__mxhosts__', form.TableSection, 'mxhost', null,
+ o = dnss.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.'));
so = ss.option(form.Value, 'domain', _('Domain'));
so.rmempty = false;
so.datatype = 'hostname';
- so.placeholder = 'example.com';
+ so.placeholder = 'example.com.';
so = ss.option(form.Value, 'relay', _('Relay'));
so.rmempty = false;
so.datatype = 'hostname';
- so.placeholder = 'relay.example.com';
+ 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('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
+ o = dnss.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.validate = validateHostname;
+ so.placeholder = 'www.example.com.';
+
+ so = ss.option(form.Value, 'target', _('Target'));
+ so.rmempty = false;
+ so.datatype = 'hostname';
+ so.placeholder = 'example.com.';
+
+ o = dnss.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;
});
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.'));
+ _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.') + '<br />' +
+ _('The netfilter components below are only regarded when running fw4.'));
ss = o.subsection;
ss.addremove = true;
ss.anonymous = true;
ss.sortable = true;
+ ss.rowcolors = true;
+ ss.nodescriptions = true;
+ ss.modaltitle = _('Edit IP set');
- so = ss.option(form.DynamicList, 'name', _('IP set'));
+ so = ss.option(form.DynamicList, 'name', _('Name of the set'));
so.rmempty = false;
+ so.editable = true;
so.datatype = 'string';
- so = ss.option(form.DynamicList, 'domain', _('Domain'));
+ so = ss.option(form.DynamicList, 'domain', _('FQDN'));
so.rmempty = false;
+ so.editable = true;
so.datatype = 'hostname';
+ so = ss.option(form.Value, 'table', _('Netfilter table name'), _('Defaults to fw4.'));
+ so.editable = true;
+ so.placeholder = 'fw4';
+ so.rmempty = true;
+
+ so = ss.option(form.ListValue, 'table_family', _('Table IP family'), _('Defaults to IPv4+6.') + ' ' + _('Can be hinted by adding 4 or 6 to the name.') + '<br />' +
+ _('Adding an IPv6 to an IPv4 set and vice-versa silently fails.'));
+ so.editable = true;
+ so.rmempty = true;
+ so.value('inet', _('IPv4+6'));
+ so.value('ip', _('IPv4'));
+ so.value('ip6', _('IPv6'));
+
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 />' +
uci.unset('dhcp', section, 'dns');
};
- so = ss.option(form.Value, 'mac',
+ //this can be a .DynamicList or a .Value with a widget and dnsmasq handles multimac OK.
+ so = ss.option(form.DynamicList, 'mac',
_('MAC address(es)'),
- _('The hardware address(es) of this entry/host, separated by spaces.') + '<br /><br />' +
+ _('The hardware address(es) of this entry/host.') + '<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)) {
- 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)
- ];
-
- 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) {
- var node = form.Value.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]),
- ipopt = this.section.children.filter(function(o) { return o.option == 'ip' })[0];
-
- node.addEventListener('cbi-dropdown-change', L.bind(function(ipopt, section_id, ev) {
- var mac = ev.detail.value.value;
- if (mac == null || mac == '' || !hosts[mac])
- return;
-
- var iphint = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
- if (iphint == null)
- return;
-
- var ip = ipopt.formvalue(section_id);
- if (ip != null && ip != '')
- return;
-
- var node = ipopt.map.findElement('id', ipopt.cbid(section_id));
- if (node)
- dom.callClassMethod(node, 'setValue', iphint);
- }, this, ipopt, section_id));
-
- return node;
+ var macs = L.toArray(uci.get('dhcp', section, 'mac'));
+ return expandAndFormatMAC(macs);
};
+ //removed jows renderwidget function which hindered multi-mac entry
so.validate = validateMACAddr.bind(so, pools);
Object.keys(hosts).forEach(function(mac) {
var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
var m = this.section.formvalue(section, 'mac'),
n = this.section.formvalue(section, 'name');
- if ((m == null || m == '') && (n == null || n == ''))
+ if ((m && !m.length > 0) && !n)
return _('One of hostname or MAC address must be specified!');
- if (value == null || value == '' || value == 'ignore')
+ if (!value || value == 'ignore')
return true;
var leases = uci.sections('dhcp', 'host');
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)';
+ _('The IPv6 interface identifier (address suffix) as hexadecimal number (max. 16 chars).'));
+ so.datatype = 'and(rangelength(0,16),hexstring)';
so = ss.option(form.DynamicList, 'tag',
_('Tag'),
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.'));
+ _('When a host matches an entry then the special tag %s is set. Use %s to match all known hosts.').format('<code>known</code>', '<code>known</code>') + '<br /><br />' +
+ _('Ignore requests from unknown machines using %s.').format('<code>!known</code>') + '<br /><br />' +
+ _('If a host matches an entry which cannot be used because it specifies an address on a different subnet, the tag %s is set.').format('<code>known-othernet</code>'));
so.value('known', _('known'));
so.value('!known', _('!known (not known)'));
so.value('known-othernet', _('known-othernet (on different subnet)'));
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 || '?'));
+ var name, display_str = generateDnsmasqInstanceEntry(val);
+ so.value(index, display_str);
});
return [
host || '-',
- lease.ip6addrs ? lease.ip6addrs.join(' ') : lease.ip6addr,
+ lease.ip6addrs ? lease.ip6addrs.join('<br />') : lease.ip6addr,
lease.duid,
exp
];