Arch Linux-based Home Router, Part V (bind)

Having a caching Domain Name System (DNS) daemon is not a hard requirement for a home router, but most commercial routers have some caching DNS capabilities, as well as providing mappings between hostnames on the local network and their IP addresses.  Arch Linux provides several options, but I went with the Berkeley Internet Name Daemon (BIND), as it is the one I'm most familiar with, and is also the most widely installed name server on the Internet (for UNIX/Linux systems).  It comes with a BSD license, as do much of the software produced by the Internet Systems Consortium.  The Arch wiki states a warning, that the ISC may not disclose zero-day vulnerabilities in BIND up to four days after they inform paying customers.  Since this is for a home router, this shouldn't be too big of a deal (this will NOT be a guide for a public BIND server).  If you are concerned, you can try one of the other DNS server options.

To begin, install the bind package.  The actual daemon executable name is named.  Much of my configuration was created in Debian, so much of the comments in the primary files I have follow Debian instructions and follow Debian structure.  Arch loads all of the configuration into one file, /etc/named.conf.  This can make it difficult to manage, but the named binary expects the configuration to exist in this file (the systemd service file does not explicitly specify a configuration file, but it could be specified with the -c option to ExecStart).  I merely created a symlink /etc/named.conf -> /etc/bind/named.conf, and it has the following contents:

include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";

This allows for clean separation between named global options and DNS zones (the top-level zones are in named.conf.local).  

named options

Here are the contents of the options file:

options {
    directory "/var/cache/bind";
    listen-on {
        127.0.0.1;
        10.20.30.254;
        172.16.87.254;
        10.11.12.254;
    };
    forwarders {
        9.9.9.9;
        8.8.8.8;
        8.8.4.4;
        1.1.1.1;
    };
    allow-query { 
        127.0.0.1; 
        10.20.30.0/24; 
        172.16.87.0/24;  
        10.11.12.0/24;
    };
    allow-recursion { 
        127.0.0.0/8; 
        10.20.30.0/24; 
        172.16.87.0/24;  
        10.11.12.0/24;
    };
    auth-nxdomain no;    # conform to RFC1035
    listen-on-v6  { none; };
};
logging {
    category lame-servers { null; };
};

The directory option is where named stores its runtime files, the administrator should make sure this directory is owned and readable/writable by the named user (the system user thenamed daemon runs as by default in Arch).  The listen-on option is the list of IPv4 addresses corresponding to the interfaces named should listen on (I've listed localhost, my LAN, IoT, and WireGuard interfaces).  The forwarders are the list of DNS servers named should consult when resolving names it doesn't know about.  Logically, your ISP's name servers should be listed here by IPv4 address, you can get these from /etc/resolv.conf when you've acquired network information from your ISP's DHCP server.  When I originally set these up, Comcast/Xfinity was my ISP, and I didn't quite trust their name servers.  I set up CloudFlare and Google DNS servers instead.  This probably didn't shield my DNS queries from my ISP (I'd need to set up some form of encryption for my forwarded DNS queries), but it means that Comcast would have less of a say in whether I could reach their competitors (Comcast owns NBC Universal, and with Net Neutrality dying under the Trump administration, I didn't trust them serving my DNS needs).  allow-query and allow-recursion sets the networks that can query or recurse through named.  The auth-nxdomain no; option I don't fully understand, but it appears to be necessary to conform to RFC1035, which describes the Internet DNS protocols and data formats.  Several other RFCs extend this RFC.  The listen-on-v6 option is set to none;, but if and when I decide to implement IPv6 on my home network I would change this (and other parts of this configuration).

named local configuration

The only other file referenced in the top-level named.conf is /etc/bind/named.conf.local, which has the following contents:

acl internals {
    127.0.0.0/8;
    10.20.30.0/24;
    172.19.87.0/24;
    10.11.12.0/24;
};
zone "ceti" {
    type master;
    file "/etc/bind/db.ceti";
    allow-update { none; };
    notify no;
};
zone "30.20.10.in-addr.arpa" {
    type master;
    notify no;
    file "/etc/bind/db.10.20";
};
zone "87.19.172.in-addr.arpa" {
    type master;
    notify no9
    file "/etc/bind/db.172.19";
};

The acl internals lists the networks that are allowed to query, this should match the networks that are listed in allowed-query in named.conf.options.  The rest are my actual DNS zones, which have their own files specified by the file directives (I've set absolute paths to the zone files, but you can just have the basenames if the zone files are in /var/named/ according to the Arch wiki).  Each file is listed as type master, which sets this named daemon as the master DNS name server for these domains.  the *.in-addr.arpa zones are special zones for reverse lookups, you'll notice that they have the first three octets of my LAN and IoT subnets in reverse dotted decimal order.

Zone files

Here is my /etc/bind/db.ceti zone file:

;
; BIND data file for local loopback interface
;
$TTL    300
@       IN      SOA     localhost. barbican.ceti. (
                        20170729        ; Serial
                         300            ; Refresh
                          300           ; Retry
                         300            ; Expire
                          300 ) ; Negative Cache TTL
                                                ;
@         IN  NS    localhost.
@         IN  A     10.20.30.254
@         IN  AAAA  ::1
barbican.ceti.              IN  A     10.20.30.254
sodium.ceti.                IN  A     10.20.30.11
lfce4.ceti.                 IN  A     10.20.30.82
lfce3.ceti.                 IN  A     10.20.30.83
lfce2.ceti.                 IN  A     10.20.30.84
lfce.ceti.                  IN  A     10.20.30.85
osmium.ceti.                IN  A     10.20.30.87
firetv.ceti.                IN  A     10.20.30.93
marantz.ceti.               IN  A     10.20.30.94
nasty.ceti.                 IN  A     10.20.30.95
ansible.ceti.               IN  A     10.255.255.92
plumbum.ceti.               IN  A     10.255.255.93
ferrum.ceti.                IN  A     10.20.30.99
radon.ceti.                 IN  A     10.20.30.77
tennessine.ceti.            IN  A     10.20.30.117
tennessine-idrac.ceti.      IN  A     10.20.30.199

I've set the default time-to-live ($TTL) to 300 seconds (five minutes).  This is probably unnecessary (the Arch wiki has this set to 7200 seconds/two hours).  For the rest of the header, see the Zone file documentation.  One important note: any time you edit this file, manually increment the serial number.  This will tell slave named servers to transfer the updated zone.  I don't have slave DNS servers (probably unnecessary for a small home network).

This zone file I've only listed IPv4 A records, but I could include CNAME, MX, TXT, or any other valid zone record type.  This zone file maps a fully-qualified domain name (FQDN) for hosts on my network, to specific IPv4 addresses.  These should match the IPv4 addresses statically leased by the dhcpd daemon (see the previous article).

The *.in-addr.arpa zone files contain PTR records for reverse IPv4 lookups.  Rather than supplying an FQDN to tools like dig or nslookup, you can provide an IPv4 address and these tools will return the FQDN matching that hostname.  These reverse mappings are set up in these zone files, like so:

; BIND reverse data file for local loopback interface
;
$TTL    300
@       IN      SOA     barbican.ceti. root.ceti. (
                 2017041502             ; Serial
                         300            ; Refresh
                          300           ; Retry
                        300             ; Expire
                         300 )  ; Negative Cache TTL
@            IN  NS             barbican.ceti.
254      IN  PTR        barbican.ceti.
82       IN  PTR        lfce4.ceti.
83       IN  PTR        lfce3.ceti.
84       IN  PTR        lfce2.ceti.
85       IN  PTR        lfce.ceti.
87       IN  PTR        osmium.ceti.
93       IN  PTR        firetv.ceti.
94       IN  PTR        marantz.ceti.
95       IN  PTR        nasty.ceti.
77       IN  PTR        radon.ceti.
117      IN  PTR        tennessine.ceti.
199      IN  PTR        tennessine-idrac.ceti.
11       IN  PTR        sodium.ceti.

Notice that these pointer records only have the last/fourth octect of the IPv4 addresses, these should match what's in the main domain zone file.

I also have a db.10 zone file, which looks like this:

;
; BIND reverse data file for local loopback interface
;
$TTL    7200
@       IN      SOA     ceti. root.ceti. (
                              2         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                         604800 )       ; Negative Cache TTL
;
@       IN      NS      ns.ceti.
20.30.254     IN  PTR      barbican
20.30.11      IN  PTR      sodium
20.30.77      IN  PTR      radon
20.30.82      IN  PTR      lfce4
20.30.83      IN  PTR      lfce3
20.30.84      IN  PTR      lfce2
20.30.85      IN  PTR      lfce
20.30.87      IN  PTR      osmium
20.30.93      IN  PTR      firetv
20.30.94      IN  PTR      marantz
20.30.95      IN  PTR      nasty
20.30.117     IN  PTR      tennessine
20.30.199     IN  PTR      tennessine-idrac

This one is probably unnecessary, since no zone in named.conf.local refers to it.  But it also shows how the DNS administrator can define the reverse lookups.

Check configuration

Before you enable and start the named daemon, it's best to check to make sure your configuration is valid (otherwise the daemon won't start).  First, check the configuration with named-checkconf:

# named-checkconf

If this doesn't return anything (and the return code is 0) then the configuration is OK.  To check the individual zones:

# named-checkzone <zone> <zone file>

So, for my ceti zone, I run the following:

# named-checkzone ceti /etc/bind/db.ceti
zone ceti/IN: loaded serial 20170729
OK

Likewise, I can check the reverse lookup zone:

# named-checkzone 30.20.10.in-addr.arpa /etc/bind/db.10.20
zone 30.20.10.in-addr.arpa/IN: loaded serial 2017041502
OK

Final thoughts

Again, this is only a minimal BIND configuration, that maps IPv4 addresses with predefined hosts on my network.  BIND can get quite complicated, and there are the options for redundant DNS servers, delegates, and slaves, all of which are out of scope for this network.  The administrator doesn't necessarily even need to install BIND on the gateway router, if they don't need to access local hosts by FQDN.  However, having a local caching DNS server can speed up DNS requests tremendously for common domains, so requests don't need to go out to the wider Internet to resolve these domains after they are first queried.

NEXT STEPS

The following articles continue this series on setting up an Arch Linux-based Home Router:

  1. Arch Linux-based Home Router, Part I
  2. Arch Linux-based Home Router, Part II (systemd-networkd and sysctl/kernel)
  3. Arch Linux-based Home Router, Part III (firewalld configuration)
  4. Arch Linux-based Home Router, Part IV (dhcpd configuration)
  5. Arch Linux-based Home Router, Part V (bind) (This article)
  6. Arch Linux-based Home Router, Part VI (DDNS)