User:Mjb/Unbound on FreeBSD 10

From Offset
Jump to navigationJump to search

This article compiles my notes on running Unbound on FreeBSD 10-STABLE in late 2015. Any questions/comments, email me directly at root (at) skew.org.

See also:


Introduction

Unbound is the BIND replacement in FreeBSD 10 and up. It is a caching resolver (DNS client) which enforces DNSSEC.

Before enabling Unbound, it's important to understand a few things.

DNS resolution overview

This is my cobbled-together understanding of domain name resolution on FreeBSD and similar systems.

libc resolver

When software wants to do a lookup of, say, "www.example.org", it uses the OS's domain name resolution APIs, usually the getaddrinfo function in the Standard C Library (libc). Unless /etc/nsswitch.conf says otherwise, first the name is looked up in an internal table based on /etc/hosts. If not found there, resolver functions are used to query the nameserver(s) listed in /etc/resolv.conf. If no server is listed there, an attempt is made to query a nameserver on the local host (127.0.0.1).

The nameserver lines in /etc/resolv.conf point to DNS servers that will be asked for the answer to your query. They may provide an authoritative answer, or they may act as clients on your behalf, connecting to other DNS servers to resolve the name "recursively". These "resolvers" (resolving nameservers) typically also cache the answers, so for a little while, subsequent answers to the same queries can come from the cache. Almost all DNS servers are caching resolvers. Some are also authoritative, providing official responses for one or more domains.

I believe, possibly in error, that a recursive resolution goes like this: the root server (which lives at a well-known IP address) is asked "who's the authoritative server for the top-level domain 'org'?", then that server is asked "who's the authoritative server for 'example.org'?", then that server is asked "who's the authoritative server for 'www.example.org'?". That last server says "me" and so then it asked "What's the IP address of 'www.example.org'?". The answer is typically cached so the same queries for "org", "example.org", and "www.example.org" don't need to be made by the first server (the one who initially queried the root) don't need to be recursive.

The libc resolver also supports relative names, meaning you can look up just "www" and if /etc/resolv.conf contains "search example.org.", the resolver will pretend you asked for "www.example.org." The trailing dot indicates the name is absolute, i.e. the "org" is a top-level domain. By default, the libc resolver assumes that names containing any dots are absolute, as if they had a trailing dot.

Forward and stub zones

A "forwarder" is a caching resolver (usually your ISP's resolver) that you can configure your own server to send queries to. Then your server will "forward first", asking that resolver to respond from its cache or else do the recursive query for you. If that server doesn't have an answer (unlikely if you're asking your ISP's online resolver for a legit domain), then your server will try doing the recursive query itself. If you don't use a forwarder, then your server just always does its own recursive queries, which is safe, but may be slower since there's no upstream cache.

A "stub zone" is an unofficial, partial zone record from an authoritative server. The idea, I think, is that your server can obtain (from the authoritative server for the parent zone) and cache a copy of the official information about what servers are authoritative for any domain you're interested in. Your server uses that info as if it got it on the fly from an authoritative server.

In Unbound, a stub zone is just a hard-coded list, rather than something fetched from elsewhere. That is, you can configure forward zones (pointers to resolvers to use for most domains), and for certain domains, you can also configure stub zones (pointers to authoritative nameservers for those domains which you don't want to rely on other resolvers to query).

On older versions of FreeBSD, instead of Unbound, I ran BIND (named), which would just use whatever I (or dhclient, if using DHCP) put in /etc/resolv.conf as the default forwarder. For doing DNSBL queries, which must not originate from major ISPs' resolvers, I further configured it such that queries for certain zones would be recursively resolved by BIND rather than forwarded. This was accomplished by setting empty forwarders for those zones, with the forward-first flag set so it would immediately fall back on resolving locally:

/* Disable forwarding for DNSBL queries */
zone "multi.uribl.com" { type forward; forward first; forwarders {}; };
zone "dnsbl.sorbs.net" { type forward; forward first; forwarders {}; };
zone "iadb.isipp.com" { type forward; forward first; forwarders {}; };
zone "zen.spamhaus.org" { type forward; forward first; forwarders {}; };

It's not yet possible to replicate this configuration in Unbound, but you can approximate it by using stub zones.

DNSSEC

DNSSEC (Domain Name Security Extensions) is a security feature based on attaching digital signatures to zone data. The signature ensures, when your resolver asks about a particular domain, that the data coming from any non-authoritative servers (like those provided by your ISP) matches the records in that domain's authoritative servers.

Upstream support required

By default, Unbound expects to be able to use DNSSEC when communicating with DNS servers, but I think in your config, you can bypass this for some some lookups. (Not sure; haven't tried.)

DNSSEC is fairly new and is still not fully supported. The DNS servers of my ISP, Comcast, do support it. But the resolver built into my router, an Apple AirPort Time Capsule (6th generation), apparently does not, at least when used with Unbound; I haven't tested with other clients. So I have to make sure Unbound does not talk to my router's built-in resolver, or else every query results in the dreaded "SERVFAIL" response.

Clock must be accurate

DNSSEC relies on time-sensitive information. If your system clock's time is too different from those of the servers Unbound talks to, DNSSEC validation—and your lookups—will fail, and Unbound won't tell you why. You just get "SERVFAIL".

As one commenter pointed out, there's a circular dependency: You need DNS to be working in order to set the clock via NTP, but Unbound will ignore info it gets from upstream DNS servers because it thinks those servers are giving it bogus DNSSEC responses from far in the future.

Possible workarounds:
  • Get time from a local NTP server identified by IP address. But where's that server getting its time from?
  • Get time from a remote NTP server identified by IP address. IP addresses change, though; you can't count on any particular NTP server having the same IP forever.
  • Configure unbound to not use DNSSEC for the zones pool.ntp.org (or whatever you use). Insecure; you won't know for sure you got the time from the real server.
  • Install a network time appliance that provides an NTP server for devices on my LAN, and which gets its time from a radio or GPS clock.

I'm basically using the second option, hard-coding one NTP server's IP address in my ntpd config, plus a pool identified by name. That way I use all of those servers when DNS is working, and just the one if not.

My setup before enabling Unbound

I have what I think is a pretty ordinary setup:

Upstream from my FreeBSD box is a router. The router is connected to the Internet via a cable modem acting as a bridge (so the router, not cable modem, has a real IP address assigned by my ISP). The router provides NAT service (i.e., it manages traffic for a private IP address-based LAN, and it's a gateway allowing outgoing traffic to the Internet). The router also provides DHCP service (i.e., it assigns IP addresses to DHCP-enabled devices on the LAN). As part of DHCP service, it tells devices what DNS server(s) to use: the internal IP address of the router itself, indicating it has a resolver built-in, just forwarding to my ISP's DNS servers by default.

When configured to use DHCP, FreeBSD runs the dhclient daemon to periodically (and at startup) obtain DHCP leases. These leases result in either dhclient or, as allowed by default, resolvconf (acting on dhclient's behalf) writing the router's local IP address into /etc/resolv.conf as the default nameserver. In /etc/resolvconf.conf, I have resolvconf configured to ignore the DHCP-assigned DNS server and instead prepend my ISP's resolvers:

For reference, this was my previous version:

# Don't forget to run 'resolvconf -u' after changing this file

# Always-good resolver IP addresses to prepend to the list
# 75.75.75.75 & 75.75.76.76 = Comcast; 8.8.8.8 = Google
name_servers="75.75.75.75 75.75.76.76 8.8.8.8"

# The DHCP server in our Apple AirPort Time Capsule (6th Gen.) assigns itself
# as the resolver, but is apparently blocking DNSSEC, so let's never use it
name_server_blacklist="10.0.1.1"

It resulted in the creation of this /etc/resolv.conf, when DHCP was enabled:

# Generated by resolvconf
search hsd1.co.comcast.net.
nameserver 75.75.75.75
nameserver 75.75.76.76
nameserver 8.8.8.8

That is, dhclient told resolvconf "search hsd1.co.comcast.net." and "nameserver 10.0.1.1", and I told resolvconf to ignore the latter and prepend my own list of nameservers.

When not using DHCP, then resolvconf, if not disabled, just writes whatever /etc/resolvconf.conf says to use. For truly static IP configuration, I believe standard operating procedure is to just put resolv_conf="/dev/null" in /etc/resolvconf.conf so that it'll only edit /dev/null (i.e., do nothing) instead of resolv.conf.

Apply patches if needed

OK, let's get started. Unfortunately, on FreeBSD 10, as of late 2015, Unbound needs some patching. This is not the case on FreeBSD 11.

Patches for FreeBSD 10 only

These patches are in 11-CURRENT but have not yet been included in FreeBSD 10-STABLE.

Prerequisites:

  • The current system's source code is in /usr/src.
  • The security/ca_root_nss port is installed (so fetch can get the patches via HTTPS).

Fetch and apply the patches:

Rebuild Unbound:

  • cd /usr/src/lib/libunbound && make obj && make depend all install
  • cd /usr/src/usr.sbin/unbound && make obj && make depend all install

Enable Unbound

Enable the service in /etc/rc.conf

Enable the local_unbound service:

  • echo 'local_unbound_enable="YES"' >> /etc/rc.conf

If I recall correctly, the local-unbound-setup scripts (which run only once) will get the nameservers for the Unbound config files from /etc/resolv.conf, which gets rewritten to point to 127.0.0.1 (localhost, i.e. where Unbound is running). If you want to instead put your preferred resolvers in /etc/rc.conf, you can do it like this:

  • echo 'local_unbound_forwarders="75.75.75.75 75.75.76.76 8.8.8.8"' >> /etc/rc.conf

I don't know if there's a limit to the number of resolvers.

Start the server

Start it up:

  • service local_unbound start

On the first start, it does some extra setup, as if you had run service local_unbound_setup. It runs the local-unbound-setup script, which you hopefully patched. In turn, that script (among other things) generates unbound.conf and also calls resolvconf -u, which generates or modifies /etc/resolv.conf, now taking into account what Unbound has said that file should contain.

This is what you should see:

Performing initial setup.
/var/unbound/forward.conf created
/var/unbound/lan-zones.conf created
/var/unbound/control.conf created
/var/unbound/unbound.conf created
original /etc/resolvconf.conf saved as /etc/resolvconf.conf.20151012.055849
original /etc/resolv.conf saved as /etc/resolv.conf.20151012.055849
Starting local_unbound.

Things that can go wrong

On FreeBSD 10, after I had some file system corruption, I started getting messages (e.g. at boot) indicating Unbound could not start:

Waiting for nameserver to start...[1479947727] unbound-control[320:0] error: connect: Connection refused for (inet_ntop error)
.[1479947728] unbound-control[323:0] error: connect: Connection refused for (inet_ntop error)
.[1479947729] unbound-control[326:0] error: connect: Connection refused for (inet_ntop error)
.[1479947730] unbound-control[329:0] error: connect: Connection refused for (inet_ntop error)
.[1479947731] unbound-control[332:0] error: connect: Connection refused for (inet_ntop error)
 giving up

Sometimes the first message was error: SSL handshake failed.

Not having DNS service was catastrophic; I could not even log in! (I think maybe UseDNS no was needed in /etc/ssh/sshd.config).

To work around Unbound's failure to start, I had to log in at the console and revert /etc/resolv.conf to my old configuration.

I never investigated this further to find out what the cause was; I just started over with a new snapshot of FreeBSD 11.

Something similar happened under FreeBSD 11. One day after a couple of power failures, unbound just wasn't running. It had started up OK, but I didn't see anything in the logs to explain why it now was off.

Trying to start it up again failed:

# service local_unbound start
May 18 23:05:21 chilled unbound: [4590:0] notice: init module 0: validator
May 18 23:05:21 chilled unbound: [4590:0] error: failed to read /root.key
May 18 23:05:21 chilled unbound: [4590:0] error: error reading auto-trust-anchor-file: /var/unbound/root.key
May 18 23:05:21 chilled unbound: [4590:0] error: validator: error in trustanchors config
May 18 23:05:21 chilled unbound: [4590:0] error: validator: could not apply configuration settings.
May 18 23:05:21 chilled unbound: [4590:0] error: module init for module validator failed
May 18 23:05:21 chilled unbound: [4590:0] fatal error: failed to setup modules
[1495170321] unbound-control[4591:0] error: connect: Connection refused for (inet_ntop error)
.[1495170323] unbound-control[4594:0] error: connect: Connection refused for (inet_ntop error)
.[1495170324] unbound-control[4597:0] error: connect: Connection refused for (inet_ntop error)
.[1495170325] unbound-control[4600:0] error: connect: Connection refused for (inet_ntop error)
.[1495170326] unbound-control[4603:0] error: connect: Connection refused for (inet_ntop error)
 giving up

I checked on /var/unbound/root.key and somehow it was now 0 bytes. However, there was a backup with the name /var/unbound/root.key.410-0. All I had to do was delete the empty /var/unbound/root.key, and try again:

  • rm /var/unbound/root.key && service local_unbound start

It started up with its usual "notice: init module 1: iterator" message, and was fine. A fresh copy of root.key was back where it belonged.

Resulting config files

/etc/resolvconf.conf

# This file was generated by local-unbound-setup.
# Modifications will be overwritten.
resolv_conf="/dev/null" # prevent updating /etc/resolv.conf
# Static DNS configuration

Looks good.

/etc/resolv.conf

This is the new /etc/resolv.conf:

# Generated by resolvconf
search hsd1.co.comcast.net.
# nameserver 75.75.75.75
# nameserver 75.75.76.76
# nameserver 8.8.8.8

nameserver 127.0.0.1
options edns0

Looks OK, I guess.

/var/unbound/unbound.conf

# This file was generated by local-unbound-setup.
# Modifications will be overwritten.
server:
        username: unbound
        directory: /var/unbound
        chroot: /var/unbound
        pidfile: /var/run/local_unbound.pid
        auto-trust-anchor-file: /var/unbound/root.key

include: /var/unbound/forward.conf
include: /var/unbound/lan-zones.conf
include: /var/unbound/control.conf
include: /var/unbound/conf.d/*.conf

Also good. For reference: not everyone splits up the config into separate files like this. Thus "unbound.conf" can be said to be a synonym for all of the files, in aggregate.

If IPv6 is disabled on your system, you will need to add (maybe in a file in /var/unbound/conf.d):

server:
        do-ipv6: no

/var/unbound/forward.conf

# This file was generated by local-unbound-setup.
# Modifications will be overwritten.
forward-zone:
        name: .
        forward-addr: 75.75.75.75
        forward-addr: 75.75.76.76
        forward-addr: 8.8.8.8

This file made use of the local_unbound_forwarders from /etc/rc.conf.

If you need to change it, edit /etc/rc.conf and then run service local_unbound setup to regenerate everything.

Test it

  • drill www.freebsd.org

The output should look like something like this:

;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 52463
;; flags: qr rd ra ; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; www.freebsd.org.     IN      A

;; ANSWER SECTION:
www.freebsd.org.        600     IN      CNAME   wfe0.ysv.freebsd.org.
wfe0.ysv.freebsd.org.   600     IN      A       8.8.178.110

;; AUTHORITY SECTION:

;; ADDITIONAL SECTION:

;; Query time: 191 msec
;; SERVER: 127.0.0.1
;; WHEN: Mon Oct 12 06:36:44 2015
;; MSG SIZE  rcvd: 72

The 127.0.0.1 indicates drill was talking to Unbound.

Debug output

If you want to see what Unbound was doing and who it was talking to, and a lot more:

  • Add local_unbound_flags="-vvvv" to /etc/rc.conf. More "v"s = more verbosity.
  • service local_unbound restart
  • tail -f /var/log/debug.log – assumes *.=debug /var/log/debug.log is in /etc/syslog.conf

There will be a lot of output, so only use those flags for actual debugging.

Setup for DNSBL lookups

Most DNSBL servers reject queries forwarded through major ISP resolvers. So with the configuration above, this does not produce the desired result:

  • host -tTXT test.uribl.com.multi.uribl.com
test.uribl.com.multi.uribl.com descriptive text "127.0.0.1 -> Query Refused.
See http://uribl.com/refused.shtml for more information [Your DNS IP: #.#.#.#]"

Unbound must do the recursive resolution itself, so that the result will be this:

test.uribl.com.multi.uribl.com descriptive text "permanent testpoint"

The best way is to just not do any forwarding at all, so Unbound works in recursive mode. It is recommended that you don't use your ISP's resolver for mail-related traffic anyway.

Option 1: Disable forwarding entirely

This is the simplest approach, but I don't do this, because I want to keep DNS as low-overhead as possible.

  • ensure /etc/resolv.conf (still) contains nameserver 127.0.0.1
  • remove the list of forwarders from /etc/rc.conf
  • pick one:
    • rename /var/unbound/forward.conf
    • delete /var/unbound/forward.conf
  • service local_unbound setup
  • service local_unbound reload (or start instead of reload, if it's not running)

After re-running the setup command, /etc/resolvconf.conf now looks like this:

# This file was generated by local-unbound-setup.
# Modifications will be overwritten.
resolv_conf="/dev/null" # prevent updating /etc/resolv.conf
unbound_conf="/var/unbound/forward.conf"
unbound_pid="/var/run/local_unbound.pid"
unbound_service="local_unbound"
unbound_restart="service local_unbound reload"

The reference to the now-deleted forward.conf is fine.

Option 2: Keep forwarding root zone, and create a stub zone for each DNSBL domain

Maybe you're forwarding because you don't have a choice, or maybe you may have a faster connection to your ISP's DNS servers than to the DNS root servers, or maybe you're trying to build a good cache in the upstream resolver. Whatever the reason, if you do keep forwarding, and want to set up an exception for DNSBL domains, read on.

So, I asked on the unbound-users list and got some good info, although the bad news is that apparently it's impossible to replicate my old BIND configuration. (Though...I didn't really get an answer.)

However, I can get something close. I just have to create a "stub zone" for each DNSBL domain, with each zone config listing the authoritative nameservers for the DNSBL domains. The only practical difference between this and my old BIND configuration is that BIND didn't need to be told the nameservers; it would look them up as needed.

So I must first get the current list of authoritative nameservers for each DNSBL domain. For example:

  • host -tNS multi.uribl.com | grep "name server " | sed 's#.* ##'

...and then I add those to unbound.conf (or files included from it) as stub-hosts under the stub-zone for that domain. For example, create /var/unbound/conf.d/uribl.conf with the contents below:

stub-zone:
  name: multi.uribl.com
  stub-host: hh.uribl.com.
  stub-host: aa.uribl.com.
  stub-host: bb.uribl.com.
  stub-host: cc.uribl.com.
  stub-host: dd.uribl.com.
  stub-host: ee.uribl.com.
  stub-host: ff.uribl.com.
  stub-host: gg.uribl.com.

In fact, this can be automated (this is in tcsh):

  • set echo_style=both
  • foreach x ( multi.uribl.com dnsbl.sorbs.net iadb.isipp.com zen.spamhaus.org )
  • set y=/var/unbound/conf.d/$x.conf && echo "Generating $y" && echo "stub-zone:\n name: $x" >! $y && host -tNS $x | grep "name server " | sed -E 's#.* (.*)# stub-host: \1#' >> $y && unset y
  • end
  • unset x

This should be scripted to keep the files updated. See below. If the stub-hosts refer to unresolvable hosts, the DNSBL lookups will fail and your mail will all get deferred or rejected with "Temporary lookup failure" messages in your mail log.

After creating the stub zone files, try it out:

  • service local_unbound reload
  • host -tTXT test.uribl.com.multi.uribl.com

And it works! I get the "permanent testpoint" text.

(I don't yet have a better place to put this info:) For now, Senderscore does not need a stub zone set up. This test should return an IP address of 127.0.0.x:

  • host -tA 2.0.0.127.bl.score.senderscore.com

Script to regenerate DNSBL stub zone configs

#!/bin/sh

# For each zone in a list of DNSBL zones, this script looks up the zone's
# current nameservers, and defines them as stub hosts for that zone in
# the local Unbound configuration. DNS lookups within that zone will then
# query those hosts directly instead of being forwarded through the
# upstream nameserver.
#
# In order to successfully look up the current info for a zone,
# we have to not have that zone set up with an outdated host list!
# Therefore we have to temporarily disable the stub zones while we
# generate the new ones.
#

[ `/usr/bin/whoami` != root ] && echo "This script must be run as root." && return 1

IFS=' '
tmpdir=/var/tmp/unbound.conf.d.old
zones="multi.uribl.com dnsbl.sorbs.net iadb.isipp.com zen.spamhaus.org b.barracudacentral.org"

echo "$0: Stopping Sendmail"
service sendmail stop 2>&1 > /dev/null

echo "$0: Moving DNSBL stub zone configs to $tmpdir"
mkdir -p $tmpdir
for x in $zones
do
  y=/var/unbound/conf.d/${x}.conf
  mv "$y" /var/tmp/unbound.conf.d.old/${x}.conf
done

echo "$0: Reloading Unbound config without DNSBL stub zones"
service local_unbound reload

for x in $zones
do
  y=/var/unbound/conf.d/${x}.conf
  echo -n "$0: Looking up info for DNSBL zone ${x}..."
  IFS=''
  nslist=$(host -tNS $x | fgrep "name server ")
  rc=$?
  if [ $rc = 0 ]
  then
    echo " Found. Writing config."
    echo -e "stub-zone:\n  name: $x" > "$y"
    echo $nslist | sed -E 's#.* (.*)#  stub-host: \1#' >> "$y"
  else
    echo " Not found; no config written."
  fi
  unset nslist
  unset y
done

echo "$0: Loading new configs into Unbound"
service local_unbound reload

echo "$0: Restarting Sendmail"
service sendmail start 2>&1 > /dev/null

echo "$0: Done."

I run this from root's crontab every 6 hours, like this:

# every 6 hours, regenerate the DNS server's stub zone configuration
05 0,6,12,18 * * * /usr/local/etc/regenerate-stub-zones

Wanted: Keep forwarding root zone, and define DNSBL domains as recursive zones

Ha! In Unbound, this option does not exist. If the root zone uses forwarders, there is no way to create exceptions for other zones to recursively resolve.

Maybe someone will add this feature someday.

Useful commands

Aside from the usual service local_unbound [start|stop|restart]...

  • service local_unbound setup – regenerate Unbound config files and run resolvconf -u
  • service local_unbound reload – validate & process changes made to Unbound's own config files
  • unbound-control dump_cache – show what's in the cache; produces a lot of output if not empty
  • unbound-control list_stubs – see what stub zones and root hints are active
  • unbound-control flush_zone . – flush entire cache
  • unbound-control flush_zone multi.uribl.com – flush cache for this one zone
  • unbound-control list_forwards – show current forwarding config