NSD, Part 2: Dynamic DNS
Hey there! In the last post, I showed you how to setup nsd
, the Name Server Daemon, an authoritative DNS server to serve records for a given domain. In this post, I'm going to talk through how to extend that configuration to support Dynamic DNS.
Normally, if you query, say, the A or AAAA records for a domain or subdomain like git.starbeamrainbowlabs.com
, it will return the same IP address that you manually set in the DNS zone file, or if you use some online service then the value you manually set there. This is fine if your IP address does not change, but becomes problematic if your IP address may change unpredictably.
The solution, as you might have guessed, lies in dynamic DNS. Dynamic DNS is a fancy word for some kind of system where the host system that a DNS record points to (e.g. compute.bobsrockets.com
) informs the DNS server about changes to its IP address.
This is done by making a network request from the host system to some kind of API that automatically updates the DNS server - usually over HTTP (though anything else could work too, but please make sure it's encrypted!).
You may already be familiar with using a HTTP API to inform your cloud-based registrar (e.g. Cloudflare, Gandi, etc) of IP address changes, but in this post we're going to set dynamic DNS up with the nsd
server we configured in the previous post mentioned above.
The first order of business is to find some software to do this. You could also write a thing yourself (see also setting up a systemd service). There are several choices, but I went with dyndnsd
(I may update this post if I ever write my own daemon for this).
Next, you need to determine what subdomain you'll use for dynamic dns. Since DNS is hierarchical, an entire subdomain is required - you can't just do dynamic DNS for, say, wiki.bobsrockets.com
- since dyndnsd
will manage it's own DNS zone file, all dynamic DNS hostnames will be under that subdomain - e.g. wiki.dyn.bobsrockets.com
.
Configuring the server
For the server, I will be assuming that the dynamic dns daemon will be running on the same server as the nsd
daemon.
For this tutorial, we'll be setting it up unencrypted. This is a security risk if you are setting it up to accept requests over the Internet rather than a local trusted network! Notes on how to fix this at the end of this post.
Since this is a Ruby-based program (which I do generally recommend avoiding since Ruby is generally an inefficient language to write a program in I've observed), first we need to install gem
, the Ruby package manager:
sudo apt install ruby ruby-rubygems ruby-dev
Then, we can install the gem
Ruby package manager:
sudo gem install dyndnsd
Now, we need to configure it. dyndnsd
is configured using a YAML (ew) configuration file. It's probably best to show an example configuration file and explain it afterwards:
# listen address and port
host: "0.0.0.0"
port: 5354
# The internal database file. We'll create this in a moment.
db: "/var/lib/dyndnsd/db.json"
# enable debug mode?
debug: false
# all hostnames are required to be cool-name.dyn.bobsrockets.com
domain: "dyn.bobsrockets.com"
# configure the updater, here we use command_with_bind_zone, params are updater-specific
updater:
name: "command_with_bind_zone"
params:
zone_file: "/etc/dyndnsd/zones/dyn.bobsrockets.com.zone"
command: "systemctl reload nsd"
ttl: "5m"
dns: "bobsrockets.com."
email_addr: "bob.bobsrockets.com"
# Users with the hostnames they are allowed to create/update
users:
computeuser: # <--- Username
password: "alongandrandomstring"
hosts:
- compute1.dyn.bobsrockets.com
computeuser2:
password: "anotherlongandrandomstring"
hosts:
- compute2.dyn.bobsrockets.com
- compute3.dyn.bobsrockets.com
...several things to note here that I haven't already noted in comments.
zone_file: "/etc/nsd/zones/dyn.bobsrockets.com.zone"
: This is the path to the zone filedyndnsd
should update.dns: "bobsrockets.com."
: This is the fully-qualified hostname with a dot at the end of the DNS server that will be serving the DNS records (i.e. thensd
server).email_addr: "bob.bobsrockets.com"
: This sets the email address of the administrator of the system, but the@
at sign is replaced with a dot.
. If your email address contains a dot.
in the user (e.g.[email protected]
), then it won't work as expected here.
Also important here is that although when dealing with domains like this it is less confusing to always require a dot .
at the end of fully qualified domain names, this is not always the case here.
Once you've written the config file,, create the directory /etc/dyndnsd
and write it to /etc/dyndnsd/dyndnsd.yaml
.
With the config file written, we now need to create and assign permissions to the data directory it will be using. Do that like so:
sudo useradd --no-create-home --system --home /var/lib/dyndnsd dyndnsd
sudo mkdir /var/lib/dyndnsd
sudo chown dyndnsd:dyndnsd /var/lib/dyndnsd
Also, we need to create the zone file and assign the correct permissions so that it can write to it:
sudo mkdir /etc/dyndnsd/zones
sudo chown dyndnsd:dyndnsd /etc/dyndnsd/zones
# symlink the zone file into the nsd zones directory. This way dyndns isn't allowed to write to all of /etc/nsd/zones - just the 1 zone file it is supposed to update.
sudo ln -s /etc/dyndnsd/zones/dyn.bobsrockets.com.zone /etc/nsd/zones/dyn.bobsrockets.com.zone
Now, we can write a systemd service file to run dyndnsd
for us:
[Unit]
Description=dyndnsd: Dynamic DNS record updater
Documentation=https://github.com/cmur2/dyndnsd
[Service]
User=dyndnsd
Group=dyndnsd
ExecStart=/usr/local/bin/dyndnsd /etc/dyndnsd/dyndnsd.yaml
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=dyndnsd
[Install]
WantedBy=multi-user.target
Save this to /etc/systemd/system/dyndnsd.service
. Then, start the daemon like so:
sudo systemctl daemon-reload
sudo systemctl enable --now dyndnsd.service
Finally, don't forget to update your firewall to allow requests through to dyndnsd
. For UFW, do this:
sudo ufw allow 5354/tcp comment dyndnsd
That completes the configuration of dyndnsd
on the server. Now we just need to update the nsd
config file to tell it about the new zone.
nsd
's config file should be at /etc/nsd/nsd.conf
. Open it for editing, and add the following to the bottom:
zone:
name: dyn.bobsrockets.com
zonefile: dyn.bobsrockets.com.zone
...and you're done on the server!
Configuring the client(s)
For the clients, all that needs doing is configuring them to make regular requests to the dyndnsd
server to keep it appraised of their IP addresses. This is done by making a HTTP request, so we can test it with curl
like this:
curl http://computeuser:[email protected]:5354/nic/update?hostname=compute1.dyn.bobsrockets.com
...where computeuser
is the username, alongandrandomstring
is the password, and compute1.dyn.bobsrockets.com
is the hostname it should update.
The server will be able to tell what the IP address is it should set for the subdomain compute1.dyn.bobsrockets.com
by the IP address of the client making the request.
The simplest way of automating this is using cron. Add the following cronjob (sudo crontab -e
to edit the crontab):
*/5 * * * * curl -sS http://computeuser:[email protected]:5354/nic/update?hostname=compute1.dyn.bobsrockets.com
....and that's it! It really is that simple. Windows users will need to setup a scheduled task instead and install curl
, but that's outside the scope of this post.
Conclusion
In this post, I've given a whistle-stop tour of setting up a simple dynamic dns server. This can be useful if a host as a dynamic IP address on a local network but it still needs a (sub)domain for some reason.
Note that this is not suitable for untrusted networks! For example, setting dyndnsd
to accept requests over the Internet is a Bad Idea, as this simple setup is not encrypted.
If you do want to set this up over an untrusted network, you must encrypt the connection to avoid nasty DNS poisoning attacks. Assuming you already have a working reverse proxy setup on the same machine (e.g. Nginx), you'll need to add a new virtual host (a server { }
block in Nginx) that reverse-proxies to your dyndnsd
daemon and sets the X-Real-IP
HTTP header, and then ensure port 5354
is closed on your firewall to prevent direct access.
This is beyond this scope of this post and slightly different depending on your setup, but if there's the demand I can blog about how to do this.