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.
- A script to run remotely using
ssh
. - A template that defines your DNS zone file (but isn’t your DNS zone file).
- A way to trigger the script from your local machine.
- 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.
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…
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.
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.
There’s also nsupdate (rfc2136) to update individual DNS records built into bind
sudo apt install ddclient
Supports many different options:
https://ddclient.net/protocols.html
And more options are added if they are free:
https://github.com/ddclient/ddclient
Probably not the cleanest solution, but often simplifies things like off-site backup servers.
;-)
While, I am running multiple binds in production environment. I still prefer using https://www.nsupdate.info/ which just awesome free service addressing exactly this problem (no affiliation, just happy user).
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.
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.
Agree, then you can get the hover text.
A while back I had to solve this problem, and came up with using Fwknop to do DDNS update requests from remote hosts. It uses nsupdate on the backend to ask BIND to update the record.
https://github.com/jp-bennett/ddns-knock
This is why I just us ngrok. Great Node.JS app/service to whip up a TCP/UDP tunnel.
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.
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 ;-)
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.