10 'require tools.widgets as widgets';
12 var callHostHints
, callDUIDHints
, callDHCPLeases
, CBILeaseStatus
, CBILease6Status
;
14 callHostHints
= rpc
.declare({
16 method
: 'getHostHints',
20 callDUIDHints
= rpc
.declare({
22 method
: 'getDUIDHints',
26 callDHCPLeases
= rpc
.declare({
28 method
: 'getDHCPLeases',
32 CBILeaseStatus
= form
.DummyValue
.extend({
33 renderWidget: function(section_id
, option_id
, cfgvalue
) {
35 E('h4', _('Active DHCP Leases')),
36 E('table', { 'id': 'lease_status_table', 'class': 'table' }, [
37 E('tr', { 'class': 'tr table-titles' }, [
38 E('th', { 'class': 'th' }, _('Hostname')),
39 E('th', { 'class': 'th' }, _('IPv4 address')),
40 E('th', { 'class': 'th' }, _('MAC address')),
41 E('th', { 'class': 'th' }, _('Lease time remaining'))
43 E('tr', { 'class': 'tr placeholder' }, [
44 E('td', { 'class': 'td' }, E('em', _('Collecting data...')))
51 CBILease6Status
= form
.DummyValue
.extend({
52 renderWidget: function(section_id
, option_id
, cfgvalue
) {
54 E('h4', _('Active DHCPv6 Leases')),
55 E('table', { 'id': 'lease6_status_table', 'class': 'table' }, [
56 E('tr', { 'class': 'tr table-titles' }, [
57 E('th', { 'class': 'th' }, _('Host')),
58 E('th', { 'class': 'th' }, _('IPv6 address')),
59 E('th', { 'class': 'th' }, _('DUID')),
60 E('th', { 'class': 'th' }, _('Lease time remaining'))
62 E('tr', { 'class': 'tr placeholder' }, [
63 E('td', { 'class': 'td' }, E('em', _('Collecting data...')))
70 function calculateNetwork(addr
, mask
) {
71 addr
= validation
.parseIPv4(String(addr
));
74 mask
= validation
.parseIPv4(network
.prefixToMask(+mask
));
76 mask
= validation
.parseIPv4(String(mask
));
78 if (addr
== null || mask
== null)
83 addr
[0] & (mask
[0] >>> 0 & 255),
84 addr
[1] & (mask
[1] >>> 0 & 255),
85 addr
[2] & (mask
[2] >>> 0 & 255),
86 addr
[3] & (mask
[3] >>> 0 & 255)
92 function generateDnsmasqInstanceEntry(data
) {
93 const nameValueMap
= new Map(Object
.entries(data
));
94 let formatString
= nameValueMap
.get('.index') + ' (' + _('Name') + (nameValueMap
.get('.anonymous') ? ': dnsmasq[' + nameValueMap
.get('.index') + ']': ': ' + nameValueMap
.get('.name'));
97 formatString
+= ', ' + _('Domain') + ': ' + data
.domain
;
100 formatString
+= ', ' + _('Local') + ': ' + data
.local
;
104 return nameValueMap
.get('.name'), formatString
;
107 function getDHCPPools() {
108 return uci
.load('dhcp').then(function() {
109 let sections
= uci
.sections('dhcp', 'dhcp'),
110 tasks
= [], pools
= [];
112 for (var i
= 0; i
< sections
.length
; i
++) {
113 if (sections
[i
].ignore
== '1' || !sections
[i
].interface)
116 tasks
.push(network
.getNetwork(sections
[i
].interface).then(L
.bind(function(section_id
, net
) {
117 var cidr
= net
? (net
.getIPAddrs()[0] || '').split('/') : null;
119 if (cidr
&& cidr
.length
== 2) {
120 var net_mask
= calculateNetwork(cidr
[0], cidr
[1]);
123 section_id
: section_id
,
124 network
: net_mask
[0],
128 }, null, sections
[i
]['.name'])));
131 return Promise
.all(tasks
).then(function() {
137 function validateHostname(sid
, s
) {
138 if (s
== null || s
== '')
142 return _('Expecting: %s').format(_('valid hostname'));
144 var labels
= s
.replace(/^\*?\.?|\.$/g, '').split(/\./);
146 for (var i
= 0; i
< labels
.length
; i
++)
147 if (!labels
[i
].match(/^[a-z0-9_](?:[a-z0-9-]{0,61}[a-z0-9])?$/i))
148 return _('Expecting: %s').format(_('valid hostname'));
153 function validateAddressList(sid
, s
) {
154 if (s
== null || s
== '')
157 var m
= s
.match(/^\/(.+)\/$/),
158 names
= m
? m
[1].split(/\//) : [ s
];
160 for (var i
= 0; i
< names
.length
; i
++) {
161 var res
= validateHostname(sid
, names
[i
]);
170 function validateServerSpec(sid
, s
) {
171 if (s
== null || s
== '')
174 var m
= s
.match(/^(\/.*\/)?(.*)$/);
176 return _('Expecting: %s').format(_('valid hostname'));
178 if (m
[1] != '//' && m
[1] != '/#/') {
179 var res
= validateAddressList(sid
, m
[1]);
184 if (m
[2] == '' || m
[2] == '#')
187 // ipaddr%scopeid#srvport@source@interface#srcport
189 m
= m
[2].match(/^([0-9a-f:.]+)(?:%[^#@]+)?(?:#(\d+))?(?:@([0-9a-f:.]+)(?:@[^#]+)?(?:#(\d+))?)?$/);
192 return _('Expecting: %s').format(_('valid IP address'));
194 if (validation
.parseIPv4(m
[1])) {
195 if (m
[3] != null && !validation
.parseIPv4(m
[3]))
196 return _('Expecting: %s').format(_('valid IPv4 address'));
198 else if (validation
.parseIPv6(m
[1])) {
199 if (m
[3] != null && !validation
.parseIPv6(m
[3]))
200 return _('Expecting: %s').format(_('valid IPv6 address'));
203 return _('Expecting: %s').format(_('valid IP address'));
206 if ((m
[2] != null && +m
[2] > 65535) || (m
[4] != null && +m
[4] > 65535))
207 return _('Expecting: %s').format(_('valid port value'));
212 function validateMACAddr(pools
, sid
, s
) {
213 if (s
== null || s
== '')
216 var leases
= uci
.sections('dhcp', 'host'),
217 this_macs
= L
.toArray(s
).map(function(m
) { return m
.toUpperCase() });
219 for (var i
= 0; i
< pools
.length
; i
++) {
220 var this_net_mask
= calculateNetwork(this.section
.formvalue(sid
, 'ip'), pools
[i
].netmask
);
225 for (var j
= 0; j
< leases
.length
; j
++) {
226 if (leases
[j
]['.name'] == sid
|| !leases
[j
].ip
)
229 var lease_net_mask
= calculateNetwork(leases
[j
].ip
, pools
[i
].netmask
);
231 if (!lease_net_mask
|| this_net_mask
[0] != lease_net_mask
[0])
234 var lease_macs
= L
.toArray(leases
[j
].mac
).map(function(m
) { return m
.toUpperCase() });
236 for (var k
= 0; k
< lease_macs
.length
; k
++)
237 for (var l
= 0; l
< this_macs
.length
; l
++)
238 if (lease_macs
[k
] == this_macs
[l
])
239 return _('The MAC address %h is already used by another static lease in the same DHCP pool').format(this_macs
[l
]);
252 network
.getNetworks()
256 render: function(hosts_duids_pools
) {
257 var has_dhcpv6
= L
.hasSystemFeature('dnsmasq', 'dhcpv6') || L
.hasSystemFeature('odhcpd'),
258 hosts
= hosts_duids_pools
[0],
259 duids
= hosts_duids_pools
[1],
260 pools
= hosts_duids_pools
[2],
261 networks
= hosts_duids_pools
[3],
264 m
= new form
.Map('dhcp', _('DHCP and DNS'),
265 _('Dnsmasq is a lightweight <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> server and <abbr title="Domain Name System">DNS</abbr> forwarder.'));
267 s
= m
.section(form
.TypedSection
, 'dnsmasq');
271 s
.tab('general', _('General Settings'));
272 s
.tab('advanced', _('Advanced Settings'));
273 s
.tab('leases', _('Static Leases'));
274 s
.tab('files', _('Resolv and Hosts Files'));
275 s
.tab('hosts', _('Hostnames'));
276 s
.tab('ipsets', _('IP Sets'));
277 s
.tab('relay', _('Relay'));
278 s
.tab('srvhosts', _('SRV'));
279 s
.tab('mxhosts', _('MX'));
280 s
.tab('cnamehosts', _('CNAME'));
281 s
.tab('pxe_tftp', _('PXE/TFTP Settings'));
283 s
.taboption('general', form
.Flag
, 'domainneeded',
284 _('Domain required'),
285 _('Do not forward DNS queries without dots or domain parts.'));
287 s
.taboption('general', form
.Flag
, 'authoritative',
289 _('This is the only DHCP server in the local network.'));
291 s
.taboption('general', form
.Value
, 'local',
293 _('Never forward matching domains and subdomains, resolve from DHCP or hosts files only.'));
295 s
.taboption('general', form
.Value
, 'domain',
297 _('Local domain suffix appended to DHCP names and hosts file entries.'));
299 o
= s
.taboption('general', form
.Flag
, 'logqueries',
301 _('Write received DNS queries to syslog.'));
304 o
= s
.taboption('general', form
.DynamicList
, 'server',
305 _('DNS forwardings'),
306 _('List of upstream resolvers to forward queries to.'));
308 o
.placeholder
= '/example.org/10.1.2.3';
309 o
.validate
= validateServerSpec
;
311 function customi18n(template
, values
) {
312 return template
.replace(/\{(\w+)\}/g, (match
, key
) => values
[key
] || match
);
315 o
= s
.taboption('general', form
.DynamicList
, 'address',
317 _('Resolve specified FQDNs to an IP.') + '<br />' +
318 customi18n(_('Syntax: {code_syntax}.'),
319 {code_syntax
: '<code>/fqdn[/fqdn…]/[ipaddr]</code>'}) + '<br />' +
320 customi18n(_('{example_nx} returns {nxdomain}.',
321 'hint: <code>/example.com/</code> returns <code>NXDOMAIN</code>.'),
322 {example_nx
: '<code>/example.com/</code>', nxdomain
: '<code>NXDOMAIN</code>'}) + '<br />' +
323 customi18n(_('{any_domain} matches any domain (and returns {nxdomain}).',
324 'hint: <code>/#/</code> matches any domain (and returns NXDOMAIN).'),
325 {any_domain
:'<code>/#/</code>', nxdomain
: '<code>NXDOMAIN</code>'}) + '<br />' +
327 _('{example_null} returns {null_addr} addresses ({null_ipv4}, {null_ipv6}) for {example_com} and its subdomains.',
328 'hint: <code>/example.com/#</code> returns NULL addresses (<code>0.0.0.0</code>, <code>::</code>) for example.com and its subdomains.'),
329 { example_null
: '<code>/example.com/#</code>',
330 null_addr
: '<code>NULL</code>',
331 null_ipv4
: '<code>0.0.0.0</code>',
332 null_ipv6
: '<code>::</code>',
333 example_com
: '<code>example.com</code>',
338 o
.placeholder
= '/router.local/router.lan/192.168.0.1';
340 o
= s
.taboption('general', form
.DynamicList
, 'ipset',
342 _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.'));
344 o
.placeholder
= '/example.org/ipset,ipset6';
346 o
= s
.taboption('general', form
.Flag
, 'rebind_protection',
347 _('Rebind protection'),
348 _('Discard upstream responses containing <a href="%s">RFC1918</a> addresses.').format('https://www.rfc-editor.org/rfc/rfc1918') + '<br />' +
349 _('Discard also upstream responses containing <a href="%s">RFC4193</a>, Link-Local and private IPv4-Mapped <a href="%s">RFC4291</a> IPv6 Addresses.').format('https://www.rfc-editor.org/rfc/rfc4193', 'https://www.rfc-editor.org/rfc/rfc4291'));
352 o
= s
.taboption('general', form
.Flag
, 'rebind_localhost',
353 _('Allow localhost'),
354 _('Exempt <code>127.0.0.0/8</code> and <code>::1</code> from rebinding checks, e.g. for RBL services.'));
355 o
.depends('rebind_protection', '1');
357 o
= s
.taboption('general', form
.DynamicList
, 'rebind_domain',
358 _('Domain whitelist'),
359 _('List of domains to allow RFC1918 responses for.'));
360 o
.depends('rebind_protection', '1');
362 o
.placeholder
= 'ihost.netflix.com';
363 o
.validate
= validateAddressList
;
365 o
= s
.taboption('general', form
.Flag
, 'localservice',
366 _('Local service only'),
367 _('Accept DNS queries only from hosts whose address is on a local subnet.'));
371 o
= s
.taboption('general', form
.Flag
, 'nonwildcard',
373 _('Bind dynamically to interfaces rather than wildcard address.'));
374 o
.default = o
.enabled
;
378 o
= s
.taboption('general', widgets
.NetworkSelect
, 'interface',
379 _('Listen interfaces'),
380 _('Listen only on the specified interfaces, and loopback if not excluded explicitly.'));
384 o
= s
.taboption('general', widgets
.NetworkSelect
, 'notinterface',
385 _('Exclude interfaces'),
386 _('Do not listen on the specified interfaces.'));
391 o
= s
.taboption('relay', form
.SectionValue
, '__relays__', form
.TableSection
, 'relay', null,
392 _('Relay DHCP requests elsewhere. OK: v4↔v4, v6↔v6. Not OK: v4↔v6, v6↔v4.')
393 + '<br />' + _('Note: you may also need a DHCP Proxy (currently unavailable) when specifying a non-standard Relay To port(<code>addr#port</code>).')
394 + '<br />' + _('You may add multiple unique Relay To on the same Listen addr.'));
402 ss
.nodescriptions
= true;
404 so
= ss
.option(form
.Value
, 'local_addr', _('Relay from'));
406 so
.datatype
= 'ipaddr';
408 for (var family
= 4; family
<= 6; family
+= 2) {
409 for (var i
= 0; i
< networks
.length
; i
++) {
410 if (networks
[i
].getName() != 'loopback') {
411 var addrs
= (family
== 6) ? networks
[i
].getIP6Addrs() : networks
[i
].getIPAddrs();
412 for (var j
= 0; j
< addrs
.length
; j
++) {
413 var addr
= addrs
[j
].split('/')[0];
414 so
.value(addr
, E([], [
416 widgets
.NetworkSelect
.prototype.renderIfaceBadge(networks
[i
]),
424 so
= ss
.option(form
.Value
, 'server_addr', _('Relay to address'));
427 so
.placeholder
= '192.168.10.1#535';
429 so
.validate = function(section
, value
) {
430 var m
= this.section
.formvalue(section
, 'local_addr'),
431 n
= this.section
.formvalue(section
, 'server_addr'),
433 if (n
!= null && n
!= '')
435 if (p
.length
> 1 && !/^[0-9]+$/.test(p
[1]))
436 return _('Expected port number.');
440 if ((m
== null || m
== '') && (n
== null || n
== ''))
441 return _('Both "Relay from" and "Relay to address" must be specified.');
443 if ((validation
.parseIPv6(m
) && validation
.parseIPv6(n
)) ||
444 validation
.parseIPv4(m
) && validation
.parseIPv4(n
))
447 return _('Address families of "Relay from" and "Relay to address" must match.')
450 so
= ss
.option(widgets
.NetworkSelect
, 'interface', _('Only accept replies via'));
453 so
.placeholder
= 'lan';
455 s
.taboption('files', form
.Flag
, 'readethers',
456 _('Use <code>/etc/ethers</code>'),
457 _('Read <code>/etc/ethers</code> to configure the DHCP server.'));
459 s
.taboption('files', form
.Value
, 'leasefile',
461 _('File to store DHCP lease information.'));
463 o
= s
.taboption('files', form
.Flag
, 'noresolv',
464 _('Ignore resolv file'));
467 o
= s
.taboption('files', form
.Value
, 'resolvfile',
469 _('File with upstream resolvers.'));
470 o
.depends('noresolv', '0');
471 o
.placeholder
= '/tmp/resolv.conf.d/resolv.conf.auto';
474 o
= s
.taboption('files', form
.Flag
, 'nohosts',
475 _('Ignore <code>/etc/hosts</code>'));
478 o
= s
.taboption('files', form
.DynamicList
, 'addnhosts',
479 _('Additional hosts files'));
481 o
.placeholder
= '/etc/dnsmasq.hosts';
483 o
= s
.taboption('advanced', form
.Flag
, 'quietdhcp',
484 _('Suppress logging'),
485 _('Suppress logging of the routine operation for the DHCP protocol.'));
488 o
= s
.taboption('advanced', form
.Flag
, 'sequential_ip',
489 _('Allocate IPs sequentially'),
490 _('Allocate IP addresses sequentially, starting from the lowest available address.'));
493 o
= s
.taboption('advanced', form
.Flag
, 'boguspriv',
495 _('Do not forward reverse lookups for local networks.'));
496 o
.default = o
.enabled
;
498 s
.taboption('advanced', form
.Flag
, 'filterwin2k',
499 _('Filter SRV/SOA service discovery'),
500 _('Filters SRV/SOA service discovery, to avoid triggering dial-on-demand links.') + '<br />' +
501 _('May prevent VoIP or other services from working.'));
503 o
= s
.taboption('advanced', form
.Flag
, 'filter_aaaa',
504 _('Filter IPv6 AAAA records'),
505 _('Remove IPv6 addresses from the results and only return IPv4 addresses.') + '<br />' +
506 _('Can be useful if ISP has IPv6 nameservers but does not provide IPv6 routing.'));
509 o
= s
.taboption('advanced', form
.Flag
, 'filter_a',
510 _('Filter IPv4 A records'),
511 _('Remove IPv4 addresses from the results and only return IPv6 addresses.'));
514 s
.taboption('advanced', form
.Flag
, 'localise_queries',
515 _('Localise queries'),
516 _('Return answers to DNS queries matching the subnet from which the query was received if multiple IPs are available.'));
518 if (L
.hasSystemFeature('dnsmasq', 'dnssec')) {
519 o
= s
.taboption('advanced', form
.Flag
, 'dnssec',
521 _('Validate DNS replies and cache DNSSEC data, requires upstream to support DNSSEC.'));
524 o
= s
.taboption('advanced', form
.Flag
, 'dnsseccheckunsigned',
525 _('DNSSEC check unsigned'),
526 _('Verify unsigned domain responses really come from unsigned domains.'));
527 o
.default = o
.enabled
;
531 s
.taboption('advanced', form
.Flag
, 'expandhosts',
533 _('Add local domain suffix to names served from hosts files.'));
535 s
.taboption('advanced', form
.Flag
, 'nonegcache',
536 _('No negative cache'),
537 _('Do not cache negative replies, e.g. for non-existent domains.'));
539 o
= s
.taboption('advanced', form
.Value
, 'serversfile',
540 _('Additional servers file'),
541 _('File listing upstream resolvers, optionally domain-specific, e.g. <code>server=1.2.3.4</code>, <code>server=/domain/1.2.3.4</code>.'));
542 o
.placeholder
= '/etc/dnsmasq.servers';
544 o
= s
.taboption('advanced', form
.Flag
, 'strictorder',
546 _('Upstream resolvers will be queried in the order of the resolv file.'));
549 o
= s
.taboption('advanced', form
.Flag
, 'allservers',
551 _('Query all available upstream resolvers.'));
554 o
= s
.taboption('advanced', form
.DynamicList
, 'bogusnxdomain',
555 _('IPs to override with NXDOMAIN'),
556 _('List of IP addresses to convert into NXDOMAIN responses.'));
558 o
.placeholder
= '64.94.110.11';
560 o
= s
.taboption('advanced', form
.Value
, 'port',
561 _('DNS server port'),
562 _('Listening port for inbound DNS queries.'));
567 o
= s
.taboption('advanced', form
.Value
, 'queryport',
569 _('Fixed source port for outbound DNS queries.'));
572 o
.placeholder
= _('any');
574 o
= s
.taboption('advanced', form
.Value
, 'dhcpleasemax',
575 _('Max. DHCP leases'),
576 _('Maximum allowed number of active DHCP leases.'));
578 o
.datatype
= 'uinteger';
579 o
.placeholder
= _('unlimited');
581 o
= s
.taboption('advanced', form
.Value
, 'ednspacket_max',
582 _('Max. EDNS0 packet size'),
583 _('Maximum allowed size of EDNS0 UDP packets.'));
585 o
.datatype
= 'uinteger';
586 o
.placeholder
= 1280;
588 o
= s
.taboption('advanced', form
.Value
, 'dnsforwardmax',
589 _('Max. concurrent queries'),
590 _('Maximum allowed number of concurrent DNS queries.'));
592 o
.datatype
= 'uinteger';
595 o
= s
.taboption('advanced', form
.Value
, 'cachesize',
596 _('Size of DNS query cache'),
597 _('Number of cached DNS entries, 10000 is maximum, 0 is no caching.'));
599 o
.datatype
= 'range(0,10000)';
600 o
.placeholder
= 1000;
602 o
= s
.taboption('pxe_tftp', form
.Flag
, 'enable_tftp',
603 _('Enable TFTP server'),
604 _('Enable the built-in single-instance TFTP server.'));
607 o
= s
.taboption('pxe_tftp', form
.Value
, 'tftp_root',
608 _('TFTP server root'),
609 _('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>.'));
610 o
.depends('enable_tftp', '1');
614 o
= s
.taboption('pxe_tftp', form
.Value
, 'dhcp_boot',
615 _('Network boot image'),
616 _('Filename of the boot image advertised to clients.'));
617 o
.depends('enable_tftp', '1');
619 o
.placeholder
= 'pxelinux.0';
621 /* PXE - https://openwrt.org/docs/guide-user/base-system/dhcp#booting_options */
622 o = s.taboption('pxe_tftp', form.SectionValue, '__pxe__', form.GridSection, 'boot', null,
623 _('Special <abbr title="Preboot eXecution Environment">PXE</abbr> boot options for Dnsmasq.'));
627 ss.nodescriptions = true;
629 so = ss.option(form.Value, 'filename',
631 _('Host requests this filename from the boot server.'));
633 so.placeholder = 'pxelinux.0';
635 so = ss.option(form.Value, 'servername',
637 _('The hostname of the boot server'));
639 so.placeholder = 'myNAS';
641 so = ss.option(form.Value, 'serveraddress',
643 _('The IP address of the boot server'));
645 so.placeholder = '192.168.1.2';
647 so = ss.option(form.DynamicList, 'dhcp_option',
649 _('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".'));
651 so.placeholder = '42,192.168.1.4';
653 so = ss.option(widgets.DeviceSelect, 'networkid',
655 _('Apply DHCP Options to this net. (Empty = all clients).'));
659 so = ss.option(form.Flag, 'force',
661 _('Always send DHCP Options. Sometimes needed, with e.g. PXELinux.'));
664 so = ss.option(form.Value, 'instance',
666 _('Dnsmasq instance to which this boot section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
669 Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
670 so.value(generateDnsmasqInstanceEntry(val));
673 o = s.taboption('srvhosts', form.SectionValue, '__srvhosts__', form.TableSection, 'srvhost', null,
674 _('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')
675 + '<br />' + _('_service: _sip, _ldap, _imap, _stun, _xmpp-client, … . (Note: while _http is possible, no browsers support SRV records.)')
676 + '<br />' + _('_proto: _tcp, _udp, _sctp, _quic, … .')
677 + '<br />' + _('You may add multiple records for the same Target.')
678 + '<br />' + _('Larger weights (of the same prio) are given a proportionately higher probability of being selected.'));
687 so = ss.option(form.Value, 'srv', _('SRV'), _('Syntax: <code>_service._proto.example.com.</code>'));
689 so.datatype = 'hostname';
690 so.placeholder = '_sip._tcp.example.com.';
692 so = ss.option(form.Value, 'target', _('Target'), _('CNAME or fqdn'));
694 so.datatype = 'hostname';
695 so.placeholder = 'sip.example.com.';
697 so = ss.option(form.Value, 'port', _('Port'));
699 so.datatype = 'port';
700 so.placeholder = '5060';
702 so = ss.option(form.Value, 'class', _('Priority'), _('Ordinal: lower comes first.'));
704 so.datatype = 'range(0,65535)';
705 so.placeholder = '10';
707 so = ss.option(form.Value, 'weight', _('Weight'));
709 so.datatype = 'range(0,65535)';
710 so.placeholder = '50';
712 o = s.taboption('mxhosts', form.SectionValue, '__mxhosts__', form.TableSection, 'mxhost', null,
713 _('Bind service records to a domain name: specify the location of services.')
714 + '<br />' + _('You may add multiple records for the same domain.'));
722 ss.nodescriptions = true;
724 so = ss.option(form.Value, 'domain', _('Domain'));
726 so.datatype = 'hostname';
727 so.placeholder = 'example.com.';
729 so = ss.option(form.Value, 'relay', _('Relay'));
731 so.datatype = 'hostname';
732 so.placeholder = 'relay.example.com.';
734 so = ss.option(form.Value, 'pref', _('Priority'), _('Ordinal: lower comes first.'));
736 so.datatype = 'range(0,65535)';
737 so.placeholder = '0';
739 o = s.taboption('cnamehosts', form.SectionValue, '__cname__', form.TableSection, 'cname', null,
740 _('Set an alias for a hostname.'));
748 ss.nodescriptions = true;
750 so = ss.option(form.Value, 'cname', _('Domain'));
752 so.datatype = 'hostname';
753 so.placeholder = 'www.example.com.';
755 so = ss.option(form.Value, 'target', _('Target'));
757 so.datatype = 'hostname';
758 so.placeholder = 'example.com.';
760 o = s.taboption('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
761 _('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.'));
769 so = ss.option(form.Value, 'name', _('Hostname'));
771 so.datatype = 'hostname';
773 so = ss.option(form.Value, 'ip', _('IP address'));
775 so.datatype = 'ipaddr';
779 Object.keys(hosts).forEach(function(mac) {
780 var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4);
782 for (var i = 0; i < addrs.length; i++)
783 ipaddrs[addrs[i]] = hosts[mac].name || mac;
786 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
787 so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
790 o = s.taboption('ipsets', form.SectionValue, '__ipsets__', form.GridSection, 'ipset', null,
791 _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.'));
799 so = ss.option(form.DynamicList, 'name', _('IP set'));
801 so.datatype = 'string';
803 so = ss.option(form.DynamicList, 'domain', _('Domain'));
805 so.datatype = 'hostname';
807 o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
808 _('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 />' +
809 _('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 />' +
810 _('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).'));
817 ss.nodescriptions = true;
819 ss.modaltitle = _('Edit static lease');
821 so = ss.option(form.Value, 'name',
823 _('Optional hostname to assign'));
824 so.validate = validateHostname;
826 so.write = function(section, value) {
827 uci.set('dhcp', section, 'name', value);
828 uci.set('dhcp', section, 'dns', '1');
830 so.remove = function(section) {
831 uci.unset('dhcp', section, 'name');
832 uci.unset('dhcp', section, 'dns');
835 so = ss.option(form.Value, 'mac',
836 _('MAC address(es)'),
837 _('The hardware address(es) of this entry/host, separated by spaces.') + '<br /><br />' +
838 _('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.'));
839 //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
840 so.validate = function(section_id, value) {
841 var macaddrs = L.toArray(value);
843 for (var i = 0; i < macaddrs.length; i++)
844 if (!macaddrs[i].match(/^([a-fA-F0-9]{2}|\*):([a-fA-F0-9]{2}:|\*:){4}(?:[a-fA-F0-9]{2}|\*)$/))
845 return _('Expecting a valid MAC address, optionally including wildcards');
850 so.cfgvalue = function(section) {
851 var macs = L.toArray(uci.get('dhcp', section, 'mac')),
854 for (var i = 0, mac; (mac = macs[i]) != null; i++)
855 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)) {
857 parseInt(RegExp.$1, 16), parseInt(RegExp.$2, 16),
858 parseInt(RegExp.$3, 16), parseInt(RegExp.$4, 16),
859 parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16)
862 result.push(m.map(function(n) { return isNaN(n) ? '*' : '%02X'.format(n) }).join(':'));
864 return result.length ? result.join(' ') : null;
866 so.renderWidget = function(section_id, option_index, cfgvalue) {
867 var node = form.Value.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]),
868 ipopt = this.section.children.filter(function(o) { return o.option == 'ip' })[0];
870 node.addEventListener('cbi-dropdown-change', L.bind(function(ipopt, section_id, ev) {
871 var mac = ev.detail.value.value;
872 if (mac == null || mac == '' || !hosts[mac])
875 var iphint = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
879 var ip = ipopt.formvalue(section_id);
880 if (ip != null && ip != '')
883 var node = ipopt.map.findElement('id', ipopt.cbid(section_id));
885 dom.callClassMethod(node, 'setValue', iphint);
886 }, this, ipopt, section_id));
890 so.validate = validateMACAddr.bind(so, pools);
891 Object.keys(hosts).forEach(function(mac) {
892 var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
893 so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
896 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.'));
897 so.value('ignore', _('Ignore'));
898 so.datatype = 'or(ip4addr,"ignore")';
899 so.validate = function(section, value) {
900 var m = this.section.formvalue(section, 'mac'),
901 n = this.section.formvalue(section, 'name');
903 if ((m == null || m == '') && (n == null || n == ''))
904 return _('One of hostname or MAC address must be specified!');
906 if (value == null || value == '' || value == 'ignore')
909 var leases = uci.sections('dhcp', 'host');
911 for (var i = 0; i < leases.length; i++)
912 if (leases[i]['.name'] != section && leases[i].ip == value)
913 return _('The IP address %h is already used by another static lease').format(value);
915 for (var i = 0; i < pools.length; i++) {
916 var net_mask = calculateNetwork(value, pools[i].netmask);
918 if (net_mask && net_mask[0] == pools[i].network)
922 return _('The IP address is outside of any DHCP pool address range');
925 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
926 so.value(ipv4, ipaddrs[ipv4] ? '%s (%s)'.format(ipv4, ipaddrs[ipv4]) : ipv4);
929 so = ss.option(form.Value, 'leasetime',
931 _('Host-specific lease time, e.g. <code>5m</code>, <code>3h</code>, <code>7d</code>.'));
933 so.value('5m', _('5m (5 minutes)'));
934 so.value('3h', _('3h (3 hours)'));
935 so.value('12h', _('12h (12 hours - default)'));
936 so.value('7d', _('7d (7 days)'));
937 so.value('infinite', _('infinite (lease does not expire)'));
939 so = ss.option(form.Value, 'duid',
941 _('The DHCPv6-DUID (DHCP unique identifier) of this host.'));
942 so.datatype = 'and(rangelength(20,36),hexstring)';
943 Object.keys(duids).forEach(function(duid) {
944 so.value(duid, '%s (%s)'.format(duid, duids[duid].hostname || duids[duid].macaddr || duids[duid].ip6addr || '?'));
947 so = ss.option(form.Value, 'hostid',
948 _('IPv6-Suffix (hex)'),
949 _('The IPv6 interface identifier (address suffix) as hexadecimal number (max. 16 chars).'));
950 so.datatype = 'and(rangelength(0,16),hexstring)';
952 so = ss.option(form.DynamicList, 'tag',
954 _('Assign new, freeform tags to this entry.'));
956 so = ss.option(form.DynamicList, 'match_tag',
958 _('When a host matches an entry then the special tag %s is set. Use %s to match all known hosts.').format('<code>known</code>',
959 '<code>known</code>') + '<br /><br />' +
960 _('Ignore requests from unknown machines using %s.').format('<code>!known</code>') + '<br /><br />' +
961 _('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>'));
962 so.value('known', _('known'));
963 so.value('!known', _('!known (not known)'));
964 so.value('known-othernet', _('known-othernet (on different subnet)'));
967 so = ss.option(form.Value, 'instance',
969 _('Dnsmasq instance to which this DHCP host section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
972 Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
973 so.value(generateDnsmasqInstanceEntry(val));
977 so = ss.option(form.Flag, 'broadcast',
979 _('Force broadcast DHCP response.'));
981 so = ss.option(form.Flag, 'dns',
982 _('Forward/reverse DNS'),
983 _('Add static forward and reverse DNS entries for this host.'));
985 o = s.taboption('leases', CBILeaseStatus, '__status__');
988 o = s.taboption('leases', CBILease6Status, '__status6__');
990 return m.render().then(function(mapEl) {
991 poll.add(function() {
992 return callDHCPLeases().then(function(leaseinfo) {
993 var leases = Array.isArray(leaseinfo.dhcp_leases) ? leaseinfo.dhcp_leases : [],
994 leases6 = Array.isArray(leaseinfo.dhcp6_leases) ? leaseinfo.dhcp6_leases : [];
996 cbi_update_table(mapEl.querySelector('#lease_status_table'),
997 leases.map(function(lease) {
1000 if (lease.expires === false)
1001 exp = E('em', _('unlimited'));
1002 else if (lease.expires <= 0)
1003 exp = E('em', _('expired'));
1005 exp = '%t'.format(lease.expires);
1007 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
1008 name = hint ? hint.name : null,
1011 if (name && lease.hostname && lease.hostname != name)
1012 host = '%s (%s)'.format(lease.hostname, name);
1013 else if (lease.hostname)
1014 host = lease.hostname;
1023 E('em', _('There are no active leases')));
1026 cbi_update_table(mapEl.querySelector('#lease6_status_table'),
1027 leases6.map(function(lease) {
1030 if (lease.expires === false)
1031 exp = E('em', _('unlimited'));
1032 else if (lease.expires <= 0)
1033 exp = E('em', _('expired'));
1035 exp = '%t'.format(lease.expires);
1037 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
1038 name = hint ? (hint.name || L.toArray(hint.ipaddrs || hint.ipv4)[0] || L.toArray(hint.ip6addrs || hint.ipv6)[0]) : null,
1041 if (name && lease.hostname && lease.hostname != name && lease.ip6addr != name)
1042 host = '%s (%s)'.format(lease.hostname, name);
1043 else if (lease.hostname)
1044 host = lease.hostname;
1050 lease.ip6addrs ? lease.ip6addrs.join(' ') : lease.ip6addr,
1055 E('em', _('There are no active leases')));