Linux-Fu: Your Own Dynamic DNS

It is a problem as old as the Internet. You want to access your computer remotely, but it is behind a router that randomly gets different IP addresses. Or maybe it is your laptop and it winds up in different locations with, again, different IP addresses. There are many ways to solve this problem and some of them are better than others.

A lot of routers can report their IP address to a dynamic DNS server. That used to be great, but now it seems like many of them hound you to upgrade or constantly renew so you can see their ads. Some of them disappear, too. If your router vendor supplies one, that might be a good choice, until you change routers, of course. OpenWRT supports many such services and there are many lists of common services.

However, if you have a single public accessible computer, for example a Web server or even a cloud instance, and you are running your own DNS server, you really don’t need one of those services. I’m going to show you how I do it with an accessible Linux server running Bind. This is a common setup, but if you have a different system you might have to adapt a bit.

There are many ways to set up dynamic DNS if you are willing to have a great deal of structure on both sides. Most of these depend on setting up a secret key to allow for DNS updates and some sort of script that calls nsupdate or having the DHCP server do it. The problem is, I have a lot of client computers and many are set up differently. I wanted a system where the only thing needed on the client side was ssh. All the infrastructure remains on the DNS server.

Background

I’m going to assume you already have Bind setup and you have a working knowledge of what DNS does. In general, though, you will have a single file for each domain — zone in DNS speak — you control. Here’s a typical zone file (RFC 1035 controls the format):

; zone file for wd5gnr.com
$TTL 3600 ; default TTL for zone
$ORIGIN wd5gnr.com.
@ 3600 IN SOA ns1.wd5gnr.com. alw.al-williams.com. (
12040320 ; serial number
3m ; refresh
180 ; retry
1209600 ; expire
3m ; negative
)
@ IN A 158.69.212.64
@ IN NS ns1
@ IN NS ns2
ns1 IN A 158.69.212.64
ns2 IN A 158.69.212.64
@ IN MX 10 mail
www IN A 158.69.212.64

The parts of interest here are the $TTL or time to live. The value is in seconds, so this is an hour. That might be a bit long to wait if your IP address changes a lot. There’s also a serial number that servers use to tell that the record changed. There’s no real format to the number as long as every change results in a larger number. You can use a sequence counter or permute the date. It doesn’t really matter. Finally, there are IN records that tell us different IP addresses. For this file, @ is a shorthand for wd5gnr.com and anything without a period at the end will have wd5gnr.com appended to it. So the last line defines a host “www.wd5gnr.com” and could have been written with “www.wd5gnr.com.” instead of “www” — that last period makes all the difference.

 

Somehow, we need a way to make more records in this zone file that will point other hosts — maybe dyn.wd5gnr.com — to a different IP address. I started out with a very simple script on the DNS server that would find the IP address of the caller and modify a template to create a DNS zone file and then reload the zone. This worked until I wanted to handle more than one dynamic host at a time. I’ll show you how I dealt with that, but first, let’s talk about what you need to do this yourself.

What You’ll Need

In addition to the DNS server for a domain you control, you’ll also need ssh access to your server set up to use a certificate and not a password. You probably need root access, too, although I’ll show you how you won’t need it after setup, if you don’t mind allowing anyone logged into your account to update your IP address.

Setting up ssh to not require a password is easy and highly recommended. If you need a primer on setting up Bind, you can read this article, as long as you remember to use your package manager in place of yum — unless your package manager is yum! Or you might prefer this one.

The Plan

Once you have your DNS server set up and an ssh session, there are only a few things to setup.

  1. A script to run remotely using ssh.
  2. A template that defines your DNS zone file (but isn’t your DNS zone file).
  3. A way to trigger the script from your local machine.
  4. A way to reload the DNS server.

The script, of course, is a Bash script but it makes good use of Awk. My original template file format was simple. I made a copy of the zone file, replace the serial number with $SERIAL and the dynamic IP address with $IP. The script would plug in new values and reload the DNS server using a control program known as rndc, more in a minute.

A Few Gotchas

The biggest problem with this scheme is that there is only one dynamic IP address allowed. But before we fix that, let’s look at some of the problems. First, we need to learn the remote address of the computer calling the script. Here’s some code:

echo "$SSH_CLIENT" | cut -d ' ' -f 1

This, of course, assumes we are logged in via ssh. I took a few shortcuts since I didn’t expect to call this script manually except during testing. The arguments were simple, although I’d later have to add a bit more. The first script took an IP address or a dash to indicate my ssh IP address, a zone name and that was it.

The script would look for ${ZONENAME}.dns.template in the current directory and use a simple awk script to gsub any occurrence of $SERIAL and $IP in the template. Awk would write to a temporary file and once successful, I’d move the file over to the DNS file (again, in the same directory) and make the server update.

That last bit is a little tricky, too. Bind has a tool, rndc, that can reload a zone (among other things). But normally you have to be root to run it. You can use sudo, of course, but for an automated script, that’s not handy since it will want your password.

Modifying SUDOERS

Turns out that sudo has a lot of features you don’t often use. One of them will allow users or groups of users to execute things with no password even if it would normally require root. Obviously, you need to be very careful with this ability. I made a file called /etc/sudoers.d/91-dynamicdns on the DNS server that has a single line in it:

myuserid    ALL=(root) NOPASSWD:   /usr/sbin/rndc reload wd5gnr.com

This allows me to reload that one zone file as my normal user without entering a password by using sudo. If you try to do anything else, you’ll still get the sudo password prompt.

XKCD by [Randall Munroe] CC By_NC 2.5

Calling the Script

All that’s left is to call the script on the local machine using ssh. You have several options. Of course, running it manually is easy enough, but I used cron or anacron to schedule execution of the script periodically and roughly in sync with the TTL value. You could use systemd, if you prefer. On a laptop using NetworkManager, it would be possible to write a script that runs when the network connection connects, which would probably be suitable.

Exactly how you do it will depend on your setup. Keep in mind that even once WiFi connects, the router could get a new WAN IP address at any time, so probably some sort of periodic update is a good idea even if you want to force an update on network connect or log in.

Multiple Hosts

That all worked fine for a while, but I eventually wanted to allow for multiple lines. The problem is that each update is disconnected from the rest. Somehow you want the template to only impact the specific lines that need to change. I also wanted to check the IP address and only reload in the case that the new IP address was different.

There are many solutions to this problem, of course. It would even be possible to store the IP addresses in a database or file and then make all updates each time. But that seemed like a pain, so I opted for something easier. The template file will now copy any line without a prefix right to the output unchanged. But some lines will have a prefix which is text followed by a colon. When the script finds such a line, it looks to see if the prefix is the one we are working with. If so, it replaces $SERIAL and $IP as before. It strips the prefix off and temporarily stashes the line. There is one special prefix, !, that matches any prefix. That’s primarily to allow $SERIAL to be used in one line that changes on each update.

With this new scheme, no output occurs as the script processes the template. However, the script now takes two arguments: the template and the existing zone file. Processing for the second file is different. It simply copies each line from the zone file to a new file unless there was a replacement line from the first pass. If there is, that line replaces the old line. You can determine which pass awk is in by looking at ARGIND which tells you which file you are currently processing.

The only big issue, then, is if you tried to run the script twice at the same time. The answer to that problem is to use flock, a technique covered in an earlier Linux-Fu.

You can find the entire final script on GitHub along with an example template file. Note that everything runs on the server. You simply run the script via ssh — and that can be done automatically in a number of ways — and the code on the server side takes care of the rest. Since you probably need ssh set up anyway, this means there are no extra keys to maintain and no updates to each client any time you want to modify anything.

Other Methods

As usual, there are many ways to solve this problem. This may or may not suit your needs. I considered just writing the template using $INCLUDE to include a sub zone file for each dynamic host and having the script write those. That will probably work but if you had a lot of hosts, it would wind up with a lot of stray files. There is also some ambiguity in RFC 1035 as to the correct behavior of $INCLUDE although the Bind documentation clears it up, at least as far as Bind is concerned.

It still shows some interesting tricks with awk and flock, though. There are other ways to find your remote servers, such as PageKite. Finally, you can throw hardware at the problem.

14 thoughts on “Linux-Fu: Your Own Dynamic DNS

  1. Before I setup my own public bind server for this, I’d rather just use SSH remote port forwarding and do not need to care about port forwarding in the NAT router at home – if that is even possible when you’re not at home but at work or in the public librarys free wifi, or…

    1. I like this approach very much, though I still need a process that checks that the reverse port forwarding is working and restart the SSH tunnel on the client if not. And you only get one (or a few specified on the command line) ports but then all I want is SSH back on a non-standard port anyway.

      1. I use the .ssh/config file to specify quite a few of (mostly remote) port forwardings.It is very convenient.
        Then i do not need to specify them on the commandline but can just do “ssh server”.

        You can use autossh to keep the connection up and running.

    1. Apologies, did not mean to hit “report comment.” I’ve had very little coffee, this morning.

      On the contrary, I wanted to thank you for the recommendation. I’m working to set up a small BBS, and looking for just such a service, and these folks seem to offer exactly what I want.

  2. Might be nice if an inline xkcd comic linked back to the original instead of linking to the image itself, which is viewable with a simple right click or tap-and-hold making the link rather redundant.

  3. I run a cron script that queries whatismyip.com, which returns my external IP address. It compares it to the previous value stored in a local text file and emails my phone if it has changed. Not as fancy, but it works for me.

  4. Why not just using DHCPd and ddns updates ? You would just need your DNS Server to get a fixed IP or preferred DHCP IP which is simpler and cleaner than having to wrote a custom bash script.

    There is also a nice trick using the magic of SOA if you don’t like to change you DNS server list within your nodes or ISP Box but that’s for another story ;-)

  5. I was setting this system up on a machine that didn’t have cron and doesn’t run all the time. It did, however, run NetworkManager. In theory, you can run a script on network changes in a subdirectory of /etc/NetworkManager/dispatcher.d. However, it appears it does NOT run in subdirectories for some reason. However, if you put the following into 10-dhcp-change and make it owned by root with permission 744 in the dispatcher.d directory it works great:

    #!/bin/bash
    # despite docs to the contrary, this won’t run in a subdirectory of dispatch.d
    if [ “$2” == dhcp4-change -o “$2” == dhcp6-change ]
    then
    runuser -u YOUR_USER — ssh YOUR_SERVER ./updateip – DYN_DOMAIN YOUR_HOSTNAME X
    fi

    Where YOUR_USER, YOUR_SERVER, DYN_DOMAIN, and YOUR_HOSTNAME are changed to match your needs. The X is the ID letter for the particular entry.

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.