Linux Router Part 2: DNS, DDNS, and DHCP

In Part 1 I described how to set up a simple and efficient router and perimeter firewall on just about any computer.

What I kind of glossed over was DNS and DHCP. The barebones solution I described will work for automatically connecting new devices to the network and allowing them to reach Internet resources as you would expect from any home router. But suppose you present network resources from devices on your own network – a NAS or server of some kind, for example: Wouldn’t it be nice to be able to reach those using their actual names rather than their IP address? And wouldn’t it be nice if anything you connected to your network got its device name automatically registered in DNS and pointing at its current IP address rather than having to manually edit your zone file and manually set an IP address for anything you might potentially want to reach?

A properly configured combination of DNS and DHCP makes this possible: The relevant configuration to achieve is that the two services trust each other so that the DNS server registers the device name the DHCP server reports back when a device receives an IP address lease.

DNS

DNS – or Domain Name System – is how our computer knows which IP address corresponds to the domain name we just typed in the address bad in our browser. To install such a service, just install BIND, as we saw in the previous article:

sudo apt install bind9

You would expect that this should pretty much be it, but for some reason I had to fight the systemd-resolved subsystem to make my router resolve its own DNS queries: In other words other devices on the network worked fine, but the router itself kept using systemd rather than Bind for its DNS queries. The short of it is that I needed to override the systemd resolver. I edited /etc/systemd/resolved.conf adding the following lines:

[Resolve]
DNS=10.199.200.1
Domains=mydomain.com

I also ensured /etc/resolve.conf pointed at the correct file instead of the default stub:

sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf

This effectively forced the router to perform real DNS lookups against the proper DNS service.

DHCP

Next we need to allow devices on our network to receive addresses from our router. First let’s install ISC’s DHCP server – again exactly what I showed in the previous article:

apt install isc-dhcp-server

Now we need a configuration file. Edit /etc/dhcp/dhcpd.conf. It’s partially pre-filled, but make sure it contains sections similar to the following:

/etc/dhcp/dhcpd.conf

option domain-name "mydomain.com";
option domain-name-servers 10.199.200.1;

default-lease-time 600;
max-lease-time 7200;

ddns-update-style none;

authoritative;

subnet 10.199.200.0 netmask 255.255.255.0 {
    range 10.199.200.100 10.199.200.254;
    option subnet-mask 255.255.255.0;
    option routers 10.199.200.1;
}

Restart the DHCP server to make the new rules take:

sudo systemctl restart isc-dhcp-server.service

OK, so to reiterate: we now have a service that performs domain name lookups for us. We also have a service that will lease IP addresses to devices that request one, and that will tell them how to reach other networks (effectively the Internet), and where to find the DNS service. Let’s take it to the next level!

The DNS Zone File

The DNS server effectively needs to carry a database of sorts, containing key parts of its configuration. This database is called a zone file. It may look a bit daunting at first, but persevere through this short introduction, and you’ll have something workable and a basic understanding of it in a short while.

We’re used to having the system configuration in the /etc directory tree, and as expected we find a directory in /etc/bind/ with a bunch of Bind-related stuff. Remember what I said about the zone file being a database, though: We want Bind to be able to update the database in runtime, and so the correct place to put our zone file is actually in /var/lib/bind/. Let’s build a skeleton file to start with:

/var/lib/bind/db.mydomain.com

$ORIGIN .
$TTL 604800	; 1 week
mydomain.com		IN SOA	gateway.mydomain.com. (
				1000       ; serial
				14400      ; refresh (4 hours)
				3600       ; retry (1 hour)
				604800     ; expire (1 week)
				300        ; minimum (5 minutes)
				)
			NS	gateway.mydomain.com.
gateway                 A       10.199.200.1

This zone file is enough to start with, and it doesn’t matter if you don’t understand it all at this point. The key parts here are a Start Of Authority record, which tells clients that this DNS server is authoritative for the mydomain.com domain.

The first value in this record is a serial number, which is relevant if we ever need to perform any manual changes to the DNS zone – something I will provide an example for further down in this article.

Next we have a Name Server record: In a larger environment you’d expect to see at least two of these for high availability purposes.

Finally you have an A – or server – record for this specific router, unimaginatively called gateway – this of course is the hostname of the router. It’s not unlikely that this is the only static record you’ll need, as most other addresses could just as well be dynamically assigned via DHCP, and if need be made semi-static using DHCP reservations.

The Reverse Zone File

DNS can not only help us with converting a human-readable hostname to an IP address that your computer can use. It can also be used for reverse lookups: If you know the IP address of a device, you can learn its hostname.

The reverse zone file is by convention named after your subnet range in reverse, and contains much of the same we see in the regular file:

/var/lib/bind/db.200.199.10.in-addr.arpa.rev

$ORIGIN .
$TTL 3600	; 1 hour
200.199.10.in-addr.arpa	IN SOA	gateway.mydomain.com. (
				1000       ; serial
				14400      ; refresh (4 hours)
				3600       ; retry (1 hour)
				604800     ; expire (1 week)
				300        ; minimum (5 minutes)
				)
			NS	gateway.mydomain.com.
$ORIGIN 200.199.10.in-addr.arpa.
1			PTR	gateway.mydomain.com.

An astute observer will see that instead of starting a host line with the name of the host, it starts with the host address in the subnet and indicates that to be a pointer to the hostname.

The trust key

As mentioned earlier, an important part of dynamically updated DNS is the trust relationship between the DHCP and the DNS services. In our setup we simply use the rndc key that’s auto generated upon installation of Bind9. In a production environment I would prefer to generate a separate key for DHCP updates, using the rndc-confgen command.

We’ll tell Bind to import this key on startup by editing /etc/bind/named.conf.local and adding the following line to the bottom of the configuration:

include "/etc/bind/rndc.key";

DHCP Zone Updates

While still editing /etc/bind/named.conf.local we’ll configure the service to use the zone file we created earlier, and to accept updates to it if properly authenticated. Add the following zone blocks to the bottom of the file:

zone "mydomain.com" {
  type master;
  notify yes;
  file "/var/lib/bind/db.mydomain.com";
  allow-update { key rndc-key; };
};

zone "200.199.10.in-addr.arpa" IN {
  type master;
  notify yes;
  file "/var/lib/bind/db.200.199.10.in-addr.arpa.rev";
  allow-update { key rndc-key; };
};

Making the DHCP server update DNS

Bind is now configured to understand its part of our network environment, and we’ve told it to allow updates to its zones provided the update request is authenticated using a key. Let’s turn to the DHCP server and add the relevant configuration:

/etc/dhcp/dhcpd.conf

option domain-name "mydomain.com";
option domain-name-servers 10.199.200.1;

default-lease-time 600;
max-lease-time 7200;

ddns-update-style standard;
update-static-leases on;
authoritative;
key "rndc-key" {
	algorithm hmac-sha256;
	secret "<thesecret>";
};
allow unknown-clients;
use-host-decl-names on;

zone mydomain.com. {
    primary 10.199.200.20;
    secondary 10.199.200.1;
    key rndc-key;
}
zone 200.199.10.in-addr.arpa. {
    primary 10.199.200.20;
    secondary 10.199.200.1;
    key rndc-key;
}

subnet 10.199.200.0 netmask 255.255.255.0 {
    range 10.199.200.100 10.199.200.254;
    option subnet-mask 255.255.255.0;
    option routers 10.199.200.1;
    option domain-name "mydomain.com";
    ddns-domainname "mydomain.com.";
    ddns-rev-domainname "in-addr.arpa.";
}

Note an important difference to the previous version of the file: In addition to the rest of the changes, we’ve switched the value for ddns-update-style from none to standard.

We’ve also added the block key “rndc-key” that contains the actual contents of /etc/bind/rndc.key – remember I wrote that in a production environment I would generate a separate key for dhcp update authentication.

Once we restart the Bind9 and ISC-DHCP-Server services, by now we should have working forward and reverse DNS with dynamic DNS updates from the DHCP server.

Addendum 1: Updating DNS manually

If, for some reason, we need to add a host to a DNS zone manually, we’ll want the DNS server to temporarily stop dynamic updates.

sudo rndc freeze mydomain.com

Once we’ve changed the relevant zone file and increased the value for serial to indicate that the zone file has changed, we reload the zone and allow dynamic updates again:

sudo rndc thaw mydomain.com

Addendum 2: Adding static DHCP leases

Sometimes we’re not content with being able to reach a server by its hostname: For example when opening a pinhole through a firewall, we may want a server to have a predictable IP address. In this case we add a host clause to /etc/dhcp/dhcpd.conf like this:

host websrv1 {
    hardware ethernet 52:54:00:de:ad:ef;
    fixed-address 10.199.200.2;
}

The hardware ethernet field is of course the server’s MAC address.

Since we’re changing the daemon’s configuration here, we need to restart it to make the change stick, with sudo systemctl restart isc-dhcp-server.