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](https://paulgorman.org/technical/dnsmasq-dnscrypt-proxy-privacy.txt.html).) ## 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. ## Links ## - https://forums.freebsd.org/threads/49131/ - http://serverfault.com/questions/18748/overriding-some-dns-entries-in-bind-for-internal-networks - https://wiki.archlinux.org/index.php/Unbound#Block_advertising - https://www.vultr.com/docs/running-nsd-and-unbound-on-openbsd-5-6 - http://man.openbsd.org/unbound.8 - http://man.openbsd.org/unbound.conf - http://man.openbsd.org/nsd.8 - https://en.wikipedia.org/wiki/Alternative_DNS_root - https://en.wikipedia.org/wiki/DNS_spoofing - https://en.wikipedia.org/wiki/TXT_record - http://pid1.com/posts/post13.html - https://blog.des.no/2013/09/local-caching-resolver-in-freebsd-10/ - https://wiki.alpinelinux.org/wiki/Setting_up_unbound_DNS_server#root-hints