DNS Filtering with Unbound (and general DNS stuff)

We want to serve DNS for LAN clients, but block or redirect results for certain domains (advertisements, maleware, etc.).

(See how to do something similar in a slightly lighter-weight way with dnsmasq.)

Refresher on DNS Basics

A DNS server can be either authoritative or recursive.

An authoritative DNS server returns DNS records for its domain of authority (its zone). Authoritative DNS servers are hierarchical. The ICANN root DNS servers sit at the top of the hierarchy, followed by the TLD nameservers (e.g. “.com”), followed by authoritative nameservers for second-level domains (“”), etc.

A recursive DNS server queries other nameservers (moving as far through the hierarchy of authoritative nameservers as necessary) on behalf of clients. It caches results, only hitting other nameservers for new lookups or when the TTL of a cached record expires.

Installing and Activating Unbound

On Debian, we install Unbound as a package:

# apt-get install unbound

…and edit /etc/unbound/unbound.conf.

On FreeBSD 10.1 and later, Unbound is installed by default.

%  uname -sr
FreeBSD 10.3-RELEASE-p7
%  whereis unbound
unbound: /usr/sbin/unbound /usr/share/man/man8/unbound.8.gz /usr/ports/dns/unbound
#  sysrc local_unbound_enable=YES
local_unbound_enable: NO -> YES
#  service local_unbound start
Performing initial setup.
Extracting forwarders from /etc/resolv.conf.
/var/unbound/forward.conf created
/var/unbound/lan-zones.conf created
/var/unbound/control.conf created
/var/unbound/unbound.conf created
/etc/resolvconf.conf created
original /etc/resolv.conf saved as /etc/resolv.conf.20160826.095904
Starting local_unbound.

FreeBSD has the shell script local-unbound-setup that does magic things to auto generate unbound.conf.

Double-check its output, especially since it sets up any nameservers found in /etc/resolv.conf as forwarders, which is often not what we want; it may be necessary to edit any nameservers apart from out of /etc/resolv.conf, delete /var/unbound/forwarders.conf, and re-run /usr/sbin/local-unbound-setup.

Because we want to provide name service to our LAN(s), create /var/unbound/conf.d/lan-allow.conf:

	access-control: allow
	access-control: allow

If the box gets its address via DHCP, resolvconf might overwrite /etc/resolv.conf. In this case, override the DHCP nameservers in /etc/dhcpclient.conf:

interface "em0" {
	supersede domain-name-servers

On FreeBSD the service is called local_unbound, so:

# service local_unbound restart

If we’re handing out DHCP leases from FreeBSD, we may want to edit /usr/local/etc/dhcpd.conf to hand out our new nameserver:

option domain-name-servers;

and restart the daemon:

# service isc-dhcpd restart


In unbound.conf:

include: /etc/unbound/blacklist.conf

The format of the blacklist file:

local-zone: "" refuse

The first line of the blacklist.conf file should be:


To periodically update our blacklist, we can have cron run something like this script:


curl  --silent --user username:*********** --output /tmp/dg-ads.tar.gz
curl  --silent --user username:*********** --output /tmp/dg-malicious.tar.gz

touch /var/unbound/whitelist

if [ -s /tmp/dg-ads.tar.gz ]
	cd /tmp
	tar -xvf /tmp/dg-ads.tar.gz

if [ -s /tmp/dg-malicious.tar.gz ]
	cd /tmp
	tar -xvf /tmp/dg-malicious.tar.gz

if [ -s /tmp/dg-ads.acl -a -s /tmp/dg-malicious.acl -a -e /var/unbound/whitelist ]
	cat /tmp/dg-ads.acl /tmp/dg-malicious.acl | sort | uniq \
	| cat - /var/unbound/whitelist /var/unbound/whitelist | sort | uniq -u \
	| sed '/^#.*/d'  `# Delete comments` \
	| sed 's/
//g'  `# Delete Windows newlines` \
	| sed -n -E '/^[a-z0-9\-\.]+/p' `# Strip any chars not valid in a domain name` \
	| sed 's/^\.//'  `# Strip starting dots (e.g. "")` \
	| awk '{print "local-zone: \""$1"\" refuse"}' \
	| sed '1 i\
' > /var/unbound/conf.d/blacklist.conf
	if unbound-checkconf /var/unbound/conf.d/blacklist.conf ; then
		unbound-control reload

If we want to redirect to rather than returning REFUSED:

	cat /tmp/dg-ads.acl /tmp/dg-malicious.acl /tmp/dg-porn.acl | sort | uniq \
	| cat - /var/unbound/whitelist /var/unbound/whitelist | sort | uniq -u \
	| sed '/^#.*/d'  `# Delete comments` \
	| sed 's/
//g'  `# Delete Windows newlines` \
	| sed -n -E '/^[a-z0-9\-\.]+/p' `# Strip any chars not valid in a domain name` \
	| sed 's/^\.//'  `# Strip starting dots (e.g. "")` \
	| awk '{print "local-zone: \""$1"\" redirect\nlocal-data: \""$1" A\""}' \
	| sed '1 i\
' > /var/unbound/conf.d/blacklist.conf


See unbound-control(8).

If not using the default config file location, it may be necessary to tell unbound-control the location of the config file (-c /var/unbound/unbound.conf).

# unbound-control status
version: 1.5.9
verbosity: 1
threads: 1
modules: 2 [ validator iterator ]
uptime: 419 seconds
options: control(ssl)
unbound (pid 22087) is running...

# unbound-control stats_noreset

DNS Spoofing (a.k.a. DNS cache poisoning)

A recursive DNS server can in some cases be tricked into caching bad records, thereby diverting clients to incorrect and potentially harmful destinations.

DNSSEC prevents many cache poisoning exploits.

Configure Windows DNS Server to Use Unbound

We might need Windows DNS servers. We can configure them to use our Unbound DNS server as a forwarder.

In the Windows DNS Manager, view the Properties of the DNS server. Go to the Forwarders tab, and enter the IP address of our Unbound DNS server.

If anything on the LAN will be querying our Unbound server directly, we may want to include this in our unbound.conf:

	name: ""

Playing with dig (or drill)

Note that FreeBSD has drill instead of dig. On Debian, drill is available in the ldnsutils package.

Just the IP:

%  dig +short

Find nameservers for a domain:

%  dig ns +short

Query a particular nameserver:

%  dig +noall +answer @            20817   IN      A

See the TTL for a record:

%  dig +noall +answer            15313   IN      A

Reverse lookup (PTR):

%  dig +short -x

Compare the TTL for a cached record at different recursive nameservers:

%  dig @ +noall +answer            86400   IN      A
%  dig @ +noall +answer            15277   IN      A

Look at any record type for a domain:

%  dig +noall +answer any            15423   IN      A            19191   IN      NS            19191   IN      NS            1191    IN      SOA 2015082641 7200 3600 1209600 3600            17      IN      TXT     "v=spf1 -all"            17      IN      TXT     "$Id: 4415 2015-08-24 20:12:23Z davids $"            19191   IN      AAAA    2606:2800:220:1:248:1893:25c8:1946

One of the points of interest in the above is the Start Of Authority record, which we see more clearly with dig’s multiline switch:

%  dig +noall +answer +multiline soa            1326 IN SOA (
								2015082641 ; serial
								7200       ; refresh (2 hours)
								3600       ; retry (1 hour)
								1209600    ; expire (2 weeks)
								3600       ; minimum (1 hour)

The SOA record includes: - the primary nameserver for the domain ( - the party responsible for the domain ( - the timestamp when the record was last modified (2015082641 ; serial) - seconds till the zone refresh (7200 ; refresh) - seconds to wait until retrying a failed zone refresh (3600 ; retry) - seconds until a zone result should not be considered authoritative (1209600 ; expire) - seconds a resolver should consider a negative result valid before requerying (3600 ; minimum)

Find which nameservers in a zone have synchronized which version of the name record (all servers are synced in this case):

%  dig +nssearch | awk '{ print $2, $4 }' 131206105 131206105 131206105 131206105

Bypass the resolver’s cache to see a worst-case full query path (results abridged):

%  dig +trace

; <<>> DiG 9.10.3-P4-Debian <<>> +trace
;; global options: +cmd
.                       3600    IN      NS
.                       3600    IN      NS
;; Received 768 bytes from in 1 ms

com.                    172800  IN      NS
com.                    172800  IN      NS
;; Received 735 bytes from in 27 ms            172800  IN      NS            172800  IN      NS
;; Received 591 bytes from in 51 ms            86400   IN      A            86400   IN      NS            86400   IN      NS
;; Received 452 bytes from in 24 ms

Look for TXT records:

%  dig +noall +answer txt             3598    IN      TXT     "v=spf1 ~all"

(An spf record indicates which servers are allowed to send mail for the domain.)

What DNS queries are happening now?

What if we want to log queries for debugging purposes?

# unbound-control -c /var/unbound/unbound.conf set_option verbosity: 1
# unbound-control -c /var/unbound/unbound.conf set_option log-queries: yes

This logs a lot of stuff, so when we’re done looking for a specific thing, turn down logging:

# unbound-control -c /var/unbound/unbound.conf set_option log-queries: no
# unbound-control -c /var/unbound/unbound.conf set_option verbosity: 0

By default, it logs to syslog.