--- /usr/local/www/services_unbound.php 2018-04-19 14:38:11.129707000 +0100 +++ /usr/local/www/services_unbound.php 2018-04-19 16:04:30.000000000 +0100 @@ -53,9 +53,6 @@ if (isset($a_unboundcfg['enable'])) { $pconfig['enable'] = true; } -if (isset($a_unboundcfg['dnssec'])) { - $pconfig['dnssec'] = true; -} if (isset($a_unboundcfg['forwarding'])) { $pconfig['forwarding'] = true; } @@ -182,7 +179,8 @@ if (!$input_errors) { $a_unboundcfg['enable'] = isset($pconfig['enable']); $a_unboundcfg['port'] = $pconfig['port']; - $a_unboundcfg['dnssec'] = isset($pconfig['dnssec']); + $a_unboundcfg['dnssec'] = isset($a_unboundcfg['dnssec']) ? $a_unboundcfg['dnssec'] : false; + $a_unboundcfg['qname'] = isset($a_unboundcfg['qname']) ? $a_unboundcfg['qname'] : 'disabled'; $a_unboundcfg['forwarding'] = isset($pconfig['forwarding']); $a_unboundcfg['regdhcp'] = isset($pconfig['regdhcp']); $a_unboundcfg['regdhcpstatic'] = isset($pconfig['regdhcpstatic']); @@ -270,6 +268,7 @@ $tab_array = array(); $tab_array[] = array(gettext("General Settings"), true, "services_unbound.php"); +$tab_array[] = array(gettext("Security and Privacy"), false, "services_unbound_security.php"); $tab_array[] = array(gettext("Advanced Settings"), false, "services_unbound_advanced.php"); $tab_array[] = array(gettext("Access Lists"), false, "/services_unbound_acls.php"); display_top_tabs($tab_array, true); @@ -322,13 +321,6 @@ ))->setHelp('The local-zone type used for the pfSense system domain (System | General Setup | Domain). Transparent is the default. Local-Zone type descriptions are available in the unbound.conf(5) manual pages.'); $section->addInput(new Form_Checkbox( - 'dnssec', - 'DNSSEC', - 'Enable DNSSEC Support', - $pconfig['dnssec'] -)); - -$section->addInput(new Form_Checkbox( 'forwarding', 'DNS Query Forwarding', 'Enable Forwarding Mode', # diff -u services_unbound_acls.ORIG.php services_unbound_acls.php --- /usr/local/www/services_unbound_acls.php 2018-04-19 14:38:26.153731000 +0100 +++ /usr/local/www/services_unbound_acls.php 2018-04-19 14:44:57.000000000 +0100 @@ -182,6 +182,7 @@ $tab_array = array(); $tab_array[] = array(gettext("General Settings"), false, "/services_unbound.php"); +$tab_array[] = array(gettext("Security and Privacy"), false, "services_unbound_security.php"); $tab_array[] = array(gettext("Advanced Settings"), false, "services_unbound_advanced.php"); $tab_array[] = array(gettext("Access Lists"), true, "/services_unbound_acls.php"); display_top_tabs($tab_array, true); --- /usr/local/www/services_unbound_advanced.php 2018-04-19 14:38:38.449865000 +0100 +++ /usr/local/www/services_unbound_advanced.php 2018-04-19 17:44:49.000000000 +0100 @@ -34,14 +34,6 @@ $config['unbound'] = array(); } -if (isset($config['unbound']['hideidentity'])) { - $pconfig['hideidentity'] = true; -} - -if (isset($config['unbound']['hideversion'])) { - $pconfig['hideversion'] = true; -} - if (isset($config['unbound']['prefetch'])) { $pconfig['prefetch'] = true; } @@ -50,10 +42,6 @@ $pconfig['prefetchkey'] = true; } -if (isset($config['unbound']['dnssecstripped'])) { - $pconfig['dnssecstripped'] = true; -} - if (isset($config['unbound']['dnsrecordcache'])) { $pconfig['dnsrecordcache'] = true; } @@ -70,6 +58,8 @@ $pconfig['infra_cache_numhosts'] = isset($config['unbound']['infra_cache_numhosts']) ? $config['unbound']['infra_cache_numhosts'] : '10000'; $pconfig['unwanted_reply_threshold'] = isset($config['unbound']['unwanted_reply_threshold']) ? $config['unbound']['unwanted_reply_threshold'] : 'disabled'; $pconfig['log_verbosity'] = isset($config['unbound']['log_verbosity']) ? $config['unbound']['log_verbosity'] : "1"; +$pconfig['proto'] = ($config['unbound']['proto'] =='tcp' || $config['unbound']['proto'] == 'udp') ? $config['unbound']['proto'] : 'both'; + if (isset($config['unbound']['disable_auto_added_access_control'])) { $pconfig['disable_auto_added_access_control'] = true; @@ -79,10 +69,6 @@ $pconfig['disable_auto_added_host_entries'] = true; } -if (isset($config['unbound']['use_caps'])) { - $pconfig['use_caps'] = true; -} - if ($_POST) { if ($_POST['apply']) { $retval = 0; @@ -94,6 +80,9 @@ unset($input_errors); $pconfig = $_POST; + if (isset($_POST['proto']) && !in_array($_POST['proto'], array('both', 'tcp', 'udp'))) { + $input_errors[] = gettext("A valid protocol must be specified."); + } if (isset($_POST['msgcachesize']) && !in_array($_POST['msgcachesize'], array('4', '10', '20', '50', '100', '250', '512'), true)) { $input_errors[] = gettext("A valid value for Message Cache Size must be specified."); } @@ -130,21 +119,8 @@ if (isset($_POST['log_verbosity']) && !in_array($_POST['log_verbosity'], array('0', '1', '2', '3', '4', '5'), true)) { $input_errors[] = gettext("A valid value must be specified for Log Level."); } - if (isset($_POST['dnssecstripped']) && !isset($config['unbound']['dnssec'])) { - $input_errors[] = gettext("Harden DNSSEC Data option can only be enabled if DNSSEC support is enabled."); - } if (!$input_errors) { - if (isset($_POST['hideidentity'])) { - $config['unbound']['hideidentity'] = true; - } else { - unset($config['unbound']['hideidentity']); - } - if (isset($_POST['hideversion'])) { - $config['unbound']['hideversion'] = true; - } else { - unset($config['unbound']['hideversion']); - } if (isset($_POST['prefetch'])) { $config['unbound']['prefetch'] = true; } else { @@ -155,11 +131,6 @@ } else { unset($config['unbound']['prefetchkey']); } - if (isset($_POST['dnssecstripped'])) { - $config['unbound']['dnssecstripped'] = true; - } else { - unset($config['unbound']['dnssecstripped']); - } if (isset($_POST['dnsrecordcache'])) { $config['unbound']['dnsrecordcache'] = true; } else { @@ -178,6 +149,12 @@ $config['unbound']['unwanted_reply_threshold'] = $_POST['unwanted_reply_threshold']; $config['unbound']['log_verbosity'] = $_POST['log_verbosity']; + if (isset($_POST['proto']) && ($_POST['proto'] == 'udp' || $_POST['proto'] == 'tcp')) { + $config['unbound']['proto'] = $_POST['proto']; + } else { + unset($config['unbound']['proto']); + } + if (isset($_POST['disable_auto_added_access_control'])) { $config['unbound']['disable_auto_added_access_control'] = true; } else { @@ -190,12 +167,6 @@ unset($config['unbound']['disable_auto_added_host_entries']); } - if (isset($_POST['use_caps'])) { - $config['unbound']['use_caps'] = true; - } else { - unset($config['unbound']['use_caps']); - } - write_config(gettext("DNS Resolver configured.")); mark_subsystem_dirty('unbound'); @@ -222,6 +193,7 @@ $tab_array = array(); $tab_array[] = array(gettext("General Settings"), false, "services_unbound.php"); +$tab_array[] = array(gettext("Security and Privacy"), false, "services_unbound_security.php"); $tab_array[] = array(gettext("Advanced Settings"), true, "services_unbound_advanced.php"); $tab_array[] = array(gettext("Access Lists"), false, "/services_unbound_acls.php"); display_top_tabs($tab_array, true); @@ -231,20 +203,6 @@ $section = new Form_Section('Advanced Resolver Options'); $section->addInput(new Form_Checkbox( - 'hideidentity', - 'Hide Identity', - 'id.server and hostname.bind queries are refused', - $pconfig['hideidentity'] -)); - -$section->addInput(new Form_Checkbox( - 'hideversion', - 'Hide Version', - 'version.server and version.bind queries are refused', - $pconfig['hideversion'] -)); - -$section->addInput(new Form_Checkbox( 'prefetch', 'Prefetch Support', 'Message cache elements are prefetched before they expire to help keep the cache up to date', @@ -259,13 +217,6 @@ ))->setHelp('This helps lower the latency of requests but does utilize a little more CPU. See: %1$sWikipedia%2$s', '', ''); $section->addInput(new Form_Checkbox( - 'dnssecstripped', - 'Harden DNSSEC Data', - 'DNSSEC data is required for trust-anchored zones.', - $pconfig['dnssecstripped'] -))->setHelp('If such data is absent, the zone becomes bogus. If Disabled and no DNSSEC data is received, then the zone is made insecure. '); - -$section->addInput(new Form_Checkbox( 'dnsrecordcache', 'Serve Expired', 'Serve cache records even with TTL of 0', @@ -280,6 +231,17 @@ ))->setHelp('Size of the message cache. The message cache stores DNS response codes and validation statuses. The Resource Record Set (RRSet) cache will automatically be set to twice this amount. The RRSet cache contains the actual RR data. The default is 4 megabytes.'); $section->addInput(new Form_Select( + 'proto', + 'Protocols', + $pconfig['proto'], + array('both' => 'Both UDP and TCP', 'udp' => 'UDP only', 'tcp' => 'TCP only') +))->setHelp('Normally the resolver will allow both UDP and TCP for incoming and outgoing data. In some cases - particularly when DNS traffic will be proxied or tunnelled ' . + '- it can be necessary to restrict the resolver to using only UDP or only TCP.%1$sThis setting should usually be left at the default, "both".%2$s', + '
', + '' +); + +$section->addInput(new Form_Select( 'outgoing_num_tcp', 'Outgoing TCP Buffers', $pconfig['outgoing_num_tcp'], @@ -393,13 +355,6 @@ $pconfig['disable_auto_added_host_entries'] ))->setHelp('By default, the primary IPv4 and IPv6 addresses of this firewall are added as records for the system domain of this firewall as configured in %1$sSystem: General Setup%2$s. This disables the auto generation of these entries.', '', ''); -$section->addInput(new Form_Checkbox( - 'use_caps', - 'Experimental Bit 0x20 Support', - 'Use 0x-20 encoded random bits in the DNS query to foil spoofing attempts.', - $pconfig['use_caps'] -))->setHelp('See the implementation %1$sdraft dns-0x20%2$s for more information.', '', ''); - $form->add($section); print($form); --- /usr/local/www/services_unbound_security.php 2018-04-19 14:39:03.217902000 +0100 +++ /usr/local/www/services_unbound_security.php 2018-04-19 17:19:08.000000000 +0100 @@ -0,0 +1,249 @@ + array( + 'descrip' => 'DNSSEC disabled', + 'dnssec_value' => false, + 'dnssecstripped_value' => false + ), + 1 => array( + 'descrip' => 'DNSSEC enabled: zones without DNSSEC data considered INSECURE', + 'dnssec_value' => true, + 'dnssecstripped_value' => false + ), + 2 => array( + 'descrip' => 'DNSSEC enabled and hardened: zones without DNSSEC data considered BOGUS', + 'dnssec_value' => true, + 'dnssecstripped_value' => true + ) +); + +$qname_opts = array( + 'disabled' => 'QNAME minimization disabled', + 'preferred' => 'QNAME minimization preferred ("best efforts")', + 'strict' => 'QNAME minimization required' +); + +if (!is_array($config['unbound'])) { + $config['unbound'] = array(); +} + +if (isset($config['unbound']['hideidentity'])) { + $pconfig['hideidentity'] = true; +} + +if (isset($config['unbound']['hideversion'])) { + $pconfig['hideversion'] = true; +} + +if (isset($a_unboundcfg['dnssec'])) { + $pconfig['dnssec'] = true; +} +if (isset($config['unbound']['dnssecstripped'])) { + $pconfig['dnssecstripped'] = true; +} + +if (isset($config['unbound']['use_caps'])) { + $pconfig['use_caps'] = true; +} + +if (array_key_exists($config['unbound']['qname'], $qname_opts)) { + $pconfig['qname'] = $config['unbound']['qname']; +} else { + $pconfig['qname'] = 'disabled'; +} + +if (isset($config['unbound']['tls_upstream'])) { + $pconfig['tls_upstream'] = true; +} + +if ($_POST) { + if ($_POST['apply']) { + $retval = 0; + $retval |= services_unbound_configure(); + if ($retval == 0) { + clear_subsystem_dirty('unbound'); + } + } else { + unset($input_errors); + $pconfig = $_POST; + + + if (!array_key_exists($_POST['dnssec_level'], $dnssec_opts)) { + $input_errors[] = gettext("Invalid DNSSEC option."); + } + + if (!array_key_exists($_POST['qname'], $qname_opts)) { + $input_errors[] = gettext("Invalid QNAME minimization option."); + } + + if (!$input_errors) { + if (isset($_POST['hideidentity'])) { + $config['unbound']['hideidentity'] = true; + } else { + unset($config['unbound']['hideidentity']); + } + if (isset($_POST['hideversion'])) { + $config['unbound']['hideversion'] = true; + } else { + unset($config['unbound']['hideversion']); + } + $config['unbound']['dnssec'] = $dnsopts[$_POST['dnssec_level']]['dnssec_value']; + if ($dnsopts[$_POST['dnssec_level']]['dnssecstripped_value']) { + $config['unbound']['dnssecstripped'] = true; + } else { + unset($config['unbound']['dnssecstripped']); + } + if (isset($_POST['use_caps'])) { + $config['unbound']['use_caps'] = true; + } else { + unset($config['unbound']['use_caps']); + } + $config['qname'] = $_POST['qname']; + if (isset($_POST['tls_upstream'])) { + $config['unbound']['tls_upstream'] = true; + } else { + unset($config['unbound']['tls_upstream']); + } + + write_config(gettext("DNS Resolver configured.")); + + mark_subsystem_dirty('unbound'); + } + } +} + +$pgtitle = array(gettext("Services"), gettext("DNS Resolver"), gettext("Security and Privacy")); +$pglinks = array("", "services_unbound.php", "@self"); +$shortcut_section = "resolver"; +include_once("head.inc"); + +if ($input_errors) { + print_input_errors($input_errors); +} + +if ($_POST['apply']) { + print_apply_result_box($retval); +} + +if (is_subsystem_dirty('unbound')) { + print_apply_box(gettext("The DNS resolver configuration has been changed.") . "
" . gettext("The changes must be applied for them to take effect.")); +} + +$tab_array = array(); +$tab_array[] = array(gettext("General Settings"), false, "services_unbound.php"); +$tab_array[] = array(gettext("Security and Privacy"), true, "services_unbound_security.php"); +$tab_array[] = array(gettext("Advanced Settings"), false, "services_unbound_advanced.php"); +$tab_array[] = array(gettext("Access Lists"), false, "/services_unbound_acls.php"); +display_top_tabs($tab_array, true); + +$form = new Form(); + +$section = new Form_Section('Resolver Security and Privacy Options'); + +$section->addInput(new Form_Checkbox( + 'hideidentity', + 'Hide Identity', + 'id.server and hostname.bind queries are refused', + $pconfig['hideidentity'] +)); + +$section->addInput(new Form_Checkbox( + 'hideversion', + 'Hide Version', + 'version.server and version.bind queries are refused', + $pconfig['hideversion'] +)); + +if (!isset($pconfig['dnssec']) || !$pconfig['dnssec']) { + $lev = 0; +} elseif (!isset($pconfig['dnssecstripped']) || !$pconfig['dnssecstripped']) { + $lev = 1; +} else { + $lev=2; +} +$section->addInput(new Form_Select( + 'dnssec_level', + 'DNSSEC enable and hardening', + $lev, + array_combine(array_keys($dnssec_opts), array_column($dnssec_opts, 'descrip')) +))->setHelp('When DNSSEC is enabled, DNS data will be required in all replies, for trust-anchored zones.
' . + 'A zone whose reply omits DNSSEC data will be treated as INSECURE, ' . + 'or as BOGUS with hardening selected.'); + +$section->addInput(new Form_Checkbox( + 'use_caps', + 'Experimental Bit 0x20 Support', + 'Use 0x-20 encoded random bits in the DNS query to foil spoofing attempts.', + $pconfig['use_caps'] +))->setHelp('See the implementation %1$sdraft dns-0x20%2$s for more information.', '', ''); + + +$section->addInput(new Form_Select( + 'QNAME minimization', + 'Minimize data sent to DNS servers', + $pconfig['qname'], + $qname_opts +))->setHelp('QNAME minimization is a privacy option that reduces the risk that a third party or major DNS servers can track domain queries.%1$s' . + '%2$sDisabled (the default for most devices on the Internet)%3$s - full query information is sent to DNS servers, for fast and efficient lookup.%4$s' . + '%2$sPreferred%3$s - The resolver attempts to query each DNS server for one level of the domain only. For example, %5$swww.example.com%6$s will ' . + 'be resolved by querying a root server for the ".com" domain, then querying the .com name server for "example.com", and finally querying the ' . + '.example.com name server for "www.example.com". The result is that higher level name servers do not see lower level subdomain targets. %5$s' . + 'Many DNS servers do not support this feature yet. The resolver will retry with normal lookup on failure.%6$s%4$s' . + '%2$sStrict%3$s - The lookup will fail if QNAME minimization can not be maintained (instead of falling back to usual lookup). ' . + '%2$sWarning: This will cause DNS lookup failure for many sites and domains!%3$s%7$s', + '' +); + +$section->addInput(new Form_Checkbox( + 'tls_upstream', + 'Use TLS for upstream queries', + '', + $pconfig['tls_upstream'] +))->setHelp('Wrap outgoing DNS queries within TLS encryption. This provides good security for the query and reply %1$sbetween this platform ' . + 'and the DNS server%2$s, but does not protect against a malicious user or logging with access to the DNS server or the local network.
' . + '%1$sWarning: not all servers can accept TLS. If a remote DNS server can not accept TLS, then the DNS query will fail.%2$s', + '', + '' +); + +$form->add($section); +print($form); + +include("foot.inc"); --- /etc/inc/unbound.inc 2018-03-19 11:27:44.000000000 +0000 +++ /etc/inc/unbound.inc 2018-04-19 18:03:11.000000000 +0100 @@ -251,6 +251,9 @@ $prefetch = isset($unboundcfg['prefetch']) ? "yes" : "no"; $prefetch_key = isset($unboundcfg['prefetchkey']) ? "yes" : "no"; $dns_record_cache = isset($unboundcfg['dnsrecordcache']) ? "yes" : "no"; + // only disallow TCP if option set to UDP and vice-versa + $do_udp = ($unboundcfg['proto'] == 'tcp') ? "no" : "yes"; + $do_tcp = ($unboundcfg['proto'] == 'udp') ? "no" : "yes"; $outgoing_num_tcp = isset($unboundcfg['outgoing_num_tcp']) ? $unboundcfg['outgoing_num_tcp'] : "10"; $incoming_num_tcp = isset($unboundcfg['incoming_num_tcp']) ? $unboundcfg['incoming_num_tcp'] : "10"; $edns_buffer_size = (!empty($unboundcfg['edns_buffer_size'])) ? $unboundcfg['edns_buffer_size'] : "4096"; @@ -267,6 +270,9 @@ $msg_cache_size = (!empty($unboundcfg['msgcachesize'])) ? $unboundcfg['msgcachesize'] : "4"; $verbosity = isset($unboundcfg['log_verbosity']) ? $unboundcfg['log_verbosity'] : 1; $use_caps = isset($unboundcfg['use_caps']) ? "yes" : "no"; + $tls_upstream = isset($unboundcfg['tls_upstream']) ? "yes" : "no"; + $qname_minimisation = ($unboundcfg['qname'] == 'preferred' || $unboundcfg['qname'] == 'strict') ? "yes" : "no"; + $qname_minimisation_strict = ($unboundcfg['qname'] == 'strict') ? "yes" : "no"; if (isset($unboundcfg['regovpnclients'])) { $openvpn_clients_conf .=<<