3011.io

Custom DNS for a local network

Usually, when a homelab grows, it starts to become impractical to remember all the IP addresses of various services running on many machines. This is where a local DNS server comes to the rescue! Here, we will look at how to set up a recursive DNS server with a local master zone that we can use for our machines.

This post is somewhat specific to a Raspberry Pi running a recent version of Raspbian, please keep that in mind in case you run into differences on your target machine. Another related note to this is that the target machine should have a static IP address configured on your local network to make sure the DNS doesn't randomly stop working after a restart.

Get BIND9

Very simple on a Raspberry Pi.

$ sudo apt update && sudo apt upgrade
$ sudo apt install bind9

It's also a good idea to install the dnsutils package to have access to the dig and nslookup command-line tools.

Basic options

The root configuration file lives at /etc/bind/named.conf. However, we will not be touching this file, as it is preconfigured to include several other configuration files with more specific purposes. The main configuration file should look like this, with the exception of some comments that are omitted here.

include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";
include "/etc/bind/named.conf.default-zones";

The first of these files is the /etc/bind/named.conf.options. This is where the generic configuration should live. We want to add forwarders so the DNS server knows where to look in case it doesn't know which address to return for a given domain name. We can do this by adding a forwarders object into the options. In this example the addresses point to Cloudflare and Google DNS servers. The amended file should contain the following confuguration when we're done. Note that we're only adding to the configuration we are not modifying any existing configuration that might have been there by default. Also keep in mind that there should be only one options block, and all the options should live in that single block.

options {
    forwarders {
        1.1.1.1;
        8.8.8.8;
    };
};

The semicolons are important in the configuration. Make sure they are everywhere, where they need to be. Once the file is saved, we can check the configuration for correctness via sudo named-checkconf. In case there are no errors (the utility prints nothing), the BIND9 server can be restarted, and it should correctly respond to DNS requests. This can be tested via nslookup.

$ sudo systamctl restart bind9
$ nslookup 3011.io $IP_ADDRESS

Don't forget to replace/set the IP_ADDRESS to the address of the machine where the server is running. Also, you should ideally test this from another machine on your local network to make sure the server is listetning on the correct interface (it should listen on all interfaces by default, but it's always better to make sure).

Adding an authoritative zone

Now it's time to set up an authoritative zone for our local network. For this purpose, we will use /etc/bind/named.conf.local. In this file, we will specify the zone, and a file containing the DNS records. Please keep in mind that you should either use a domain that is unresolvable via forwarders (e.g. ending with .home, but NOT .local), or a domain that you own. In this example, I am using lab.3011.io, since it's a subdomain of a domain I currently own. Note that the file with DNS records should live in a common directory, e.g. /etc/bind/zones or /etc/bind/master-zones.

zone "lab.3011.io" {
    type master;
    file "/etc/bind/master-zones/lab.3011.io.zone";
};

Now it's time to specify the zone file with DNS records. The first two lines specify the default TTL for all records, and the base domain name (so we don't need to write the FQDN everywhere).

Next, the SOA record specifies the basic properties of the current zone. The values here are pretty much default, as specified on the BIND9 documentation. After that, we specify the records for the name server (the current machine).

Once we are done with that, we can specify the records for whatever machines live on the local network. Please keep in mind that you need to adjust the IP addresses to you local network, the addresses in the following config are examples, and will most likely not work for you.

; Required configuration
$TTL 2d
$ORIGIN lab.3011.io.

@       IN      SOA     ns.lab.3011.io.     your.email.com. (
                            2025080800 ; serial number
                            12h        ; refresh
                            15m        ; update retry
                            4d         ; expiry
                            2h         ; minimum TTL
                        )
        IN      NS      ns.lab.3011.io.
ns      IN      A       10.10.0.5

; Your records can go here, examples below
forgejo IN      A       10.10.0.25
hoard   IN      A       10.10.0.15

One more note, keep in mind that the serial number is a 10 digit number that should be increased every time the configuration is changed. Usually it's the current date with two zeroes at the end. In case you need to change the configuration multiple times on one day, you should increment the two zeroes as needed.

Finally, we can test the server again. We can just restart the server, or check the configuration, the same as before. Once that is done, the result of a nslookup should look like this.

$ nslookup hoard.lab.3011.io 10.10.0.5
Server:         10.10.0.5
Address:        10.10.0.5#53

Name:   hoard.lab.3011.io
Address: 10.10.0.15

Setting up an ACL

One thing to keep in mind is that the DNS server shouldn't respond to arbitrary machines that make requests. A server like this should respond only to requests from the local network, which is why it's definitely a good idea to set up an access control list. The following is a minimal configuration in /etc/bind/named.conf.options for allowing the server to respond only to the specific subnet it lives in. Again, we're only adding to the configuration, the existing configuration should stay as-is.

acl bogus-nets {
    0.0.0.0/8;
    192.0.2.0/24;
    224.0.0.0/3;
    172.16.0.0/12;
    192.168.0.0/16;
};

acl allowed-nets {
    10.10.0.0/24;
};

options {
    allow-query {
        allowed-nets;
    };
    allow-recursion {
        allowed-nets;
    };

    blackhole {
        bogus-nets;
    };
};

The ACL configuration can get much more complicated, but this should be good enough for a simple local DNS. For more information on securing the DNS server, refer to the documentation.

Logging

By default, BIND9 logs all messages into the syslog. To change this, we can add the top-level logging block beneath the options block in /etc/bind/named.conf.options. These settings are quite complicated, but a basic overview follows. For a more detailed description, please refer to the documentation.

The channel block specifies a log sink, this can be the syslog, or in this case it is a file at a specific location. Versions specify the how many files should be kept in the rotating log, and size specifies the maximum file size before rotating the logs. The print options spefify extra information that each log line should contain.

The category specifies where some type of messages should be logged. In this case, this means we want the default category (i.e. all categories logged by default) to be appended to the log file. The queries category is a bit special as it is not logged by default (so it isn't included in the default category).

logging {
    channel custom_log {
        file "/var/cache/bind/bind.log" versions 3 size 100m;
        severity info;

        print-time yes;
        print-category yes;
        print-severity yes;
    };

    category default {
        custom_log;
    };

    category queries {
        custom_log;
    };
};

Note that this kind of logging setup should be temporary, and only for testing purposes, especially on machines like the Raspberry Pi, where these logs can speed up the wear of the SD card.

Going live

Once all the configuration is done, all machines on the network should use the DNS server we've set up. To do this, all devices with a static IP address should be reconfigured to use that DNS. After that, the DHCP server should be configured to specify the server we've set up for all devices with a dynamically assigned IP address. Please note that it's best to avoid setting a "secondary DNS" (unless it's another server with a similar configuration), since there is no real ordering of the DNS servers, and the client device is allowed to use them in any order it wishes.


Extra notes

Some extra information that isn't strictly necessary to get the server to work, but good to know anyway.

Default options

The default content of the /etc/bind/named.conf.options file looks like this (again with comments left out). The directory option sets the working directory of the daemon, where it will store its cache, keys, and other files it needs to work. The dnssec-validation is set to auto, there are also on/off options, but those are either unsafe, or require manual configuration. To the best of my knowledge, this option is already default anyway, so it's redundant, but I keep it in my config anyway, just in case. And finally, the listen-on-v6 block with the all statement enable the server to listen on all IPv6 interfaces.

options {
    directory "/var/cache/bind";
    dnssec-validation auto;

    listen-on-v6 { any; };
};