paulgorman.org/technical

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 (“example.com”), 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 127.0.0.1 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:

server:
	interface: 10.0.1.1
	interface: 10.0.2.1
	access-control: 10.0.1.0/24 allow
	access-control: 10.0.2.0/24 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 127.0.0.1
}

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 10.0.1.1;

and restart the daemon:

# service isc-dhcpd restart

Blocking

In unbound.conf:

include: /etc/unbound/blacklist.conf

The format of the blacklist file:

local-zone: "example.com" refuse

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

server:

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

#!/bin/sh

curl  --silent --user username:*********** --output /tmp/dg-ads.tar.gz http://www.squidblacklist.org/downloads/squidblacklists/dg/dg-ads.tar.gz
curl  --silent --user username:*********** --output /tmp/dg-malicious.tar.gz http://www.squidblacklist.org/downloads/squidblacklists/dg/dg-malicious.tar.gz

touch /var/unbound/whitelist

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

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

if [ -s /tmp/dg-ads.acl -a -s /tmp/dg-malicious.acl -a -e /var/unbound/whitelist ]
then
	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. ".example.com")` \
	| awk '{print "local-zone: \""$1"\" refuse"}' \
	| sed '1 i\
server:
' > /var/unbound/conf.d/blacklist.conf
	if unbound-checkconf /var/unbound/conf.d/blacklist.conf ; then
		unbound-control reload
	fi
fi

If we want to redirect to 127.0.0.1 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. ".example.com")` \
	| awk '{print "local-zone: \""$1"\" redirect\nlocal-data: \""$1" A 127.0.0.1\""}' \
	| sed '1 i\
server:
' > /var/unbound/conf.d/blacklist.conf

unbound-control

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
thread0.num.queries=5
thread0.num.cachehits=1
thread0.num.cachemiss=4
thread0.num.prefetch=0
thread0.num.recursivereplies=4
thread0.requestlist.avg=0
thread0.requestlist.max=0
thread0.requestlist.overwritten=0
thread0.requestlist.exceeded=0
thread0.requestlist.current.all=0
thread0.requestlist.current.user=0
thread0.recursion.time.avg=0.293604
thread0.recursion.time.median=0.349525
thread0.tcpusage=0
total.num.queries=5
total.num.cachehits=1
total.num.cachemiss=4
total.num.prefetch=0
total.num.recursivereplies=4
total.requestlist.avg=0
total.requestlist.max=0
total.requestlist.overwritten=0
total.requestlist.exceeded=0
total.requestlist.current.all=0
total.requestlist.current.user=0
total.recursion.time.avg=0.293604
total.recursion.time.median=0.349525
total.tcpusage=0
time.now=1472176370.069576
time.up=235.308012
time.elapsed=235.308012

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.

https://technet.microsoft.com/en-us/library/cc754941%28v=ws.11%29.aspx https://technet.microsoft.com/en-us/library/cc730756%28v=ws.11%29.aspx

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

forward-zone:
	name: "mylan.example.com"
	forward-addr: 10.0.0.2
	forward-addr: 10.0.0.3

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 example.com
93.184.216.34

Find nameservers for a domain:

%  dig ns +short example.com
b.iana-servers.net.
a.iana-servers.net.

Query a particular nameserver:

%  dig +noall +answer example.com @8.8.8.8
example.com.            20817   IN      A       93.184.216.34

See the TTL for a record:

%  dig +noall +answer example.com
example.com.            15313   IN      A       93.184.216.34

Reverse lookup (PTR):

%  dig +short -x 8.8.8.8
google-public-dns-a.google.com.

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

%  dig @10.0.0.1 +noall +answer example.com
example.com.            86400   IN      A       93.184.216.34
%  dig @8.8.8.8 +noall +answer example.com
example.com.            15277   IN      A       93.184.216.34

Look at any record type for a domain:

%  dig +noall +answer any example.com
example.com.            15423   IN      A       93.184.216.34
example.com.            19191   IN      NS      b.iana-servers.net.
example.com.            19191   IN      NS      a.iana-servers.net.
example.com.            1191    IN      SOA     sns.dns.icann.org. noc.dns.icann.org. 2015082641 7200 3600 1209600 3600
example.com.            17      IN      TXT     "v=spf1 -all"
example.com.            17      IN      TXT     "$Id: example.com 4415 2015-08-24 20:12:23Z davids $"
example.com.            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 example.com
example.com.            1326 IN SOA sns.dns.icann.org. noc.dns.icann.org. (
								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 (sns.dns.icann.org) - the party responsible for the domain (noc.dns.icann.org) - 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 google.com +nssearch | awk '{ print $2, $4 }'
ns2.google.com. 131206105
ns3.google.com. 131206105
ns1.google.com. 131206105
ns2.google.com. 131206105

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

%  dig +trace example.com

; <<>> DiG 9.10.3-P4-Debian <<>> +trace example.com
;; global options: +cmd
.                       3600    IN      NS      k.root-servers.net.
.                       3600    IN      NS      e.root-servers.net.
;; Received 768 bytes from 10.0.0.2#53(10.0.0.2) in 1 ms

com.                    172800  IN      NS      f.gtld-servers.net.
com.                    172800  IN      NS      d.gtld-servers.net.
;; Received 735 bytes from 198.41.0.4#53(a.root-servers.net) in 27 ms

example.com.            172800  IN      NS      a.iana-servers.net.
example.com.            172800  IN      NS      b.iana-servers.net.
;; Received 591 bytes from 192.35.51.30#53(f.gtld-servers.net) in 51 ms

example.com.            86400   IN      A       93.184.216.34
example.com.            86400   IN      NS      a.iana-servers.net.
example.com.            86400   IN      NS      b.iana-servers.net.
;; Received 452 bytes from 199.43.135.53#53(a.iana-servers.net) in 24 ms

Look for TXT records:

%  dig +noall +answer txt google.com
google.com.             3598    IN      TXT     "v=spf1 include:_spf.google.com ~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
ok
# unbound-control -c /var/unbound/unbound.conf set_option log-queries: yes
ok

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
ok
# unbound-control -c /var/unbound/unbound.conf set_option verbosity: 0
ok

By default, it logs to syslog.