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