DNS at home

First let me clarify what I'm talking about in this article.  This article is about running recursive DNS servers for your home LAN.  If you're still not sure what I'm talking about sit back and have a quick read, but know that this article isn't really for you.

This article will cover setting up unbound(8) as a recursive DNS server running on OpenBSD.  Since unbound(8) come installed in the base system this make things quite convenient.  

Configure Recursive DNS Server

First, lets look at the server config and explain the options.

server:
  # load additional modules. These will be used for RPZ below
  module-config: "respip validator iterator"

  # listen on IPv4/6 Loopback interfaces
  interface: 127.0.0.1
  interface: ::1
  # listen on the local lan interface
  interface: 192.0.2.5

  # allow queies from loopback addresses
  access-control: 127.0.0.0/8 allow
  access-control: ::1 allow
  # deny from everyelse
  access-control: 0.0.0.0/0 refuse
  access-control: ::0/0 refuse
  # but allow our LAN network
  access-control: 192.0.2.0/24 allow
  # don't log queries... their logged in the replies
  log-queries: no
  log-replies: yes
  log-tag-queryreply: yes
  log-local-actions: yes
  log-servfail: yes
  # prevent the use of CHAOS reporting
  hide-identity: yes
  hide-version: yes
  # Automatically download the root.hints file
  tls-cert-bundle: "/etc/ssl/cert.pem"
  root-hints: "/var/unbound/db/root.hints"
  # download the autotrust key file
  # this is what makes DNSSEC possible
  auto-trust-anchor-file: "/var/unbound/db/root.key"
  val-log-level: 2
  # see unbound.conf(5)
  aggressive-nsec: yes

  # these networks are private
  # queries regarding these networks about these networks
  # and replies returned for public names are not allowed
  # to contain these IP.  (private names ok)
  private-address: 10.0.0.0/8
  private-address: 172.16.0.0/12
  private-address: 192.168.0.0/16
  private-address: 169.254.0.0/16
  private-address: 192.0.2.0/24
  private-address: 198.51.100.0/24
  private-address: 203.0.113.0/24
  private-address: 240.0.0.0/4
  private-address: 255.255.255.255/32
  private-address: fd00::/8
  private-address: fe80::/10
  private-address: ::ffff:0:0/96
  private-address: 2001:db8::/32
  private-domain: "example.lan"

remote-control:
        control-enable: yes
        control-interface: /var/run/unbound.sock

# include local domains
include: "/var/unbound/db/localhost.zone"
include: "/var/unbound/db/internal.zone"
include: "/var/unbound/db/lan.zone"
include: "/var/unbound/db/example.lan.zone"
/var/unbound/etc/unbound.conf

There is a lot going on in that file.  Take a moment to understand the commands.  If something doesn't apply to your situation, then comment it out.

Below is the localhost zone file.

# Default zone information
local-zone: "localhost." redirect
local-data: "localhost. 10800 IN NS localhost."
local-data: "localhost. 10800 IN SOA localhost. nobody.invalid. 1 3600 1200 604800 10800"
local-data: "localhost. 10800 IN A 127.0.0.1"
local-data: "localhost. 10800 IN AAAA ::1"
local-zone: "127.in-addr.arpa." static
local-data: "127.in-addr.arpa. 10800 IN NS localhost."
local-data: "127.in-addr.arpa. 10800 IN SOA localhost. nobody.invalid. 1 3600 1200 604800 10800"
local-data: "1.0.0.127.in-addr.arpa. 10800 IN PTR localhost."
local-zone: "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa." static
local-data: "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa. 10800 IN NS localhost."
local-data: "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa. 10800 IN SOA localhost. nobody.invalid. 1 3600 1200 604800 10800"
local-data: "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa. 10800 IN PTR localhost."
/var/unbound/db/localhost.zone

Here is a zone file containing IP ranges and domains that should never be on the Internet.

# Internal zone information
# see RFC 6303, 6304, 6890, 7534, 7535

# RFC 1918
local-zone: "10.in-addr.arpa." static
local-data: "10.in-addr.arpa. 10800 IN NS @"
local-data: "10.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "16.172.in-addr.arpa." static
local-data: "16.172.in-addr.arpa. 10800 IN NS @"
local-data: "16.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "17.172.in-addr.arpa." static
local-data: "17.172.in-addr.arpa. 10800 IN NS @"
local-data: "17.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "18.172.in-addr.arpa." static
local-data: "18.172.in-addr.arpa. 10800 IN NS @"
local-data: "18.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "19.172.in-addr.arpa." static
local-data: "19.172.in-addr.arpa. 10800 IN NS @"
local-data: "19.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "20.172.in-addr.arpa." static
local-data: "20.172.in-addr.arpa. 10800 IN NS @"
local-data: "20.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "21.172.in-addr.arpa." static
local-data: "21.172.in-addr.arpa. 10800 IN NS @"
local-data: "21.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "22.172.in-addr.arpa." static
local-data: "22.172.in-addr.arpa. 10800 IN NS @"
local-data: "22.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "23.172.in-addr.arpa." static
local-data: "23.172.in-addr.arpa. 10800 IN NS @"
local-data: "23.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "24.172.in-addr.arpa." static
local-data: "24.172.in-addr.arpa. 10800 IN NS @"
local-data: "24.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "25.172.in-addr.arpa." static
local-data: "25.172.in-addr.arpa. 10800 IN NS @"
local-data: "25.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "26.172.in-addr.arpa." static
local-data: "26.172.in-addr.arpa. 10800 IN NS @"
local-data: "26.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "27.172.in-addr.arpa." static
local-data: "27.172.in-addr.arpa. 10800 IN NS @"
local-data: "27.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "28.172.in-addr.arpa." static
local-data: "28.172.in-addr.arpa. 10800 IN NS @"
local-data: "28.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "29.172.in-addr.arpa." static
local-data: "29.172.in-addr.arpa. 10800 IN NS @"
local-data: "29.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "30.172.in-addr.arpa." static
local-data: "30.172.in-addr.arpa. 10800 IN NS @"
local-data: "30.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "31.172.in-addr.arpa." static
local-data: "31.172.in-addr.arpa. 10800 IN NS @"
local-data: "31.172.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "168.192.in-addr.arpa." static
local-data: "168.192.in-addr.arpa. 10800 IN NS @"
local-data: "168.192.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
# RFC6890
local-zone: "254.169.in-addr.arpa." static
local-data: "254.169.in-addr.arpa. 10800 IN NS @"
local-data: "254.169.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
# RFC7535
local-zone: "empty.as112.arpa." static
local-data: "empty.as112.arpa. 10800 IN NS @"
local-data: "empty.as112.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "hostname.as112.arpa." static
local-data: "hostname.as112.arpa. 10800 IN NS @"
local-data: "hostname.as112.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"

local-zone: "2.0.192.in-addr.arpa." static
local-data: "2.0.192.in-addr.arpa. 10800 IN NS @"
local-data: "2.0.192.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "100.51.198.in-addr.arpa." static
local-data: "100.51.198.in-addr.arpa. 10800 IN NS @"
local-data: "100.51.198.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "113.0.203.in-addr.arpa." static
local-data: "113.0.203.in-addr.arpa. 10800 IN NS @"
local-data: "113.0.203.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "255.255.255.255.in-addr.arpa." static
local-data: "255.255.255.255.in-addr.arpa. 10800 IN NS @"
local-data: "255.255.255.255.in-addr.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "d.f.ip6.arpa." static
local-data: "d.f.ip6.arpa. 10800 IN NS @"
local-data: "d.f.ip6.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "8.e.f.ip6.arpa." static
local-data: "8.e.f.ip6.arpa. 10800 IN NS @"
local-data: "8.e.f.ip6.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "9.e.f.ip6.arpa." static
local-data: "9.e.f.ip6.arpa. 10800 IN NS @"
local-data: "9.e.f.ip6.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "a.e.f.ip6.arpa." static
local-data: "a.e.f.ip6.arpa. 10800 IN NS @"
local-data: "a.e.f.ip6.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "b.e.f.ip6.arpa." static
local-data: "b.e.f.ip6.arpa. 10800 IN NS @"
local-data: "b.e.f.ip6.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
local-zone: "8.d.b.0.1.0.0.2.ip6.arpa." static
local-data: "8.d.b.0.1.0.0.2.ip6.arpa. 10800 IN NS @"
local-data: "8.d.b.0.1.0.0.2.ip6.arpa. 10800 IN SOA @ nobody.invalid. 1 3600 1200 604800 10800"
/var/unbound/db/internal.zone
# lan. zone
# Does not exist on the Internet
# makes us authoritative for this TLD
local-zone: "lan." static
local-data: "lan. 10800 IN SOA 10 nobody.invalid. 600 1200 1800 600"
local-data: "lan. 10800 IN NS ns1.example.lan."
local-data: "example.lan. 600 IN NS ns1.example.lan."
local-data: "ns1.example.lan. 600 IN A 192.0.2.5"
/var/unbound/db/lan.zone

And now finally the internal zone.

# Internal zone
local-zone: "example.lan." static
local-data: "example.lan. 600 IN SOA 100 nobody.invalid. 600 1200 1800 600"
local-data: "example.lan. 600 IN NS ns1.example.lan."
local-data: "router.example.lan. 600 IN A 192.0.2.1"
local-data: "router.example.lan. 600 IN A 192.0.2.5"
local-data: "server1.example.lan. 600 IN A 192.0.2.10"
/var/unbound/db/example.lan.zone

Once those files are in place enable unbound and start it.

rcctl enable unbound
rcctl start unbound

Assuming that the system has internet connection test the server

# dig +short @127.0.0.1 ns1.example.lan. A
192.0.2.5
# dig +short @127.0.0.1 google.com. A
172.217.4.46

Next steps

Now that we are resolving external names what else can we do?  Well how about filter advertisements? spam? undesirable sites? Will this remove 100% of them?  Of course.... not.  But it will get many of them.  In order to achieve this filter make use of the concept of Response Policy Zones (RPZ).  RPZ are basically "overrides" that the DNS server respond with some predefined information instead of going out to look up the actual information.  RPZ's can be used to filter any hostname/domain that you want.  These can be advertisement sites?  These can be phishing and virus sites?  Whatever you would like.

To setup an RPZ add the following to the end of the unbound.conf(5) file.

rpz:
  name: "rpz.example.lan."
  zonefile: "/var/unbound/db/example.lan.rpz"
  rpz-log: yes

Now include the example.lan.rpz file in the database directory.  A couple of things to note in the file.  The both the domain "baddomain.com" and its subdomains "*.baddomain.com" are CNAME'd to the "." root zone.  This will return a NXDOMAIN response.  Surprisingly there is not a lot of information on this feature.  The best location I found was this article.

$TTL 600
$ORIGIN rpz.example.lan.
baddomain.com CNAME .
*.baddomain.com CNAME .
# add additional domains...

Now that you can block domains, where do you get the list of domains?  Here are some locations to start with.  Please abide by any ToS that the site has.

Caveats

Search Engines

Google utilizes it's adsense infrastructure to provide you with the "best" result based upon your search terms.  If you are block adsense domains, then clicking on this links returned by Google will lead to broken links and a poor spouse approval factor.

URL Shorteners

URL Shorteners can be problematic and get around some block lists by hiding making their destinations harder track down.  In another article I'll explore a method with RPZ + transparent proxy to add a measure of protection.