http:// qmail.jms1.net / djbdns / dyndns.shtml

Creating dynamic DNS names

Many people need to set up "dynamic" hostnames, which change automatically to point to whatever public IP a specific machine might have at that point in time. Dynamic hostnames are commonly used by people who want to host web sites or email on a cable, DSL, or other connection whose IP address changes over time; it's also used by people who travel and may need a hostname in order to use a two-way camera conferencing program; there are many uses for a dynamic hostname.

There are commercial services like DynDNS and No-IP which offer this as a service, and some of them will give you a certain number of hostnames for free. I've done this in the past, it works pretty well.

The problem with services like DynDNS is that the dynamic hostname has to be within a domain name that they own, such as "dyndns.org". If you own your own domain names, it might be nice to have the dynamic names exist within your domain. These services can do that, but they charge money for it, they require you to transfer your domain to their nameservers, and then you become limited to whatever capabilities their web interface gives you. For example, they may not allow you to manually set the TTL values on specific records, create records of any arbitrary type, create records which are long enough for things like SPF or domainkeys, or allow you to immediately reload the data in the live nameserver as soon as you make changes.

I don't know about you, but I don't like the idea of somebody else doing my DNS for me. A big part of doing my own DNS is that I'm able to control exactly what records get served, what their TTL values are, and control when updates happen and how the propagation works.

The directions below will show you how to set up a dynamic hostname system on your own server. This depends on your running tinydns, and being the authoritative nameserver for your domain(s).

The overall system consists of several parts. In order of execution, they are:

Again, the list above is presented in the order in which an update request "flows" from one system to another. The instructions below are presented in the order in which things should be set up; doing things in this order will allow you to test things before "going live" with the system.


The update-dyndns service

The update-dyndns service consists of two programs. The first one, "pipe-watcher", listens to a named pipe and gathers a single line of input. It then runs the second program, sending the line it just received to the "standard in" channel of that program. This is the same script I use as part of the qmail-updater service.

The second program is the "update-dyndns" program itself. It is configured with a list of hostnames and keys. When it runs, it reads a hostname, an IP address, and a key from its "standard in" channel. If the hostname is on its list, the key matches the name, and the IP address is different from the current value, it writes a text file containing a tinydns "data" line which creates an "A" record for the hostname, pointing to the indicated IP address, and then runs an external "trigger" script which should rebuild the tinydns "data" file.

In order for this to work, we will need to configure the tinydns service to build its data.cdb file using multiple inputs, instead of only using the data file. We will also need to build a "trigger" script which allows non-root processes to force a rebuild of the tinydns data.cdb file.

This service should run as a non-root user, just in case. The scripts are pre-written to use the dnsrun user, which should already exist if you're using djbdns on the machine. If not, you will need to choose or create a non-root userid as which to run the service.

Create the service directory structure

Start by creating the service directory structure and downloading the scripts.

# cd /var/service
# mkdir -m 755 update-dyndns
# cd update-dyndns
# mkdir -m 755 data log
# wget -O log/run http://qmail.jms1.net/scripts/service-any-log-run
...
# wget http://qmail.jms1.net/scripts/pipe-watcher
...
# wget -O run http://qmail.jms1.net/djbdns/service-update-dyndns-run
...
# wget http://qmail.jms1.net/djbdns/update-dyndns
...
# chmod 755 pipe-watcher run update-dyndns log/run

File: update-dyndns
Size: 6,376 bytes
Date: 2008-05-27 02:03:49 +0000
MD5: b116cb30a293ff18710e413c88cfbd8e
SHA-1: a8ce7b8f4b13ea5424c8c6a865b2129695eea03a
RIPEMD-160: 48613be6337ec34ef4ce24881a0848d181801e33
PGP Signature: update-dyndns.asc
File: service-update-dyndns-run
Size: 101 bytes
Date: 2008-05-23 01:55:43 +0000
MD5: 251d50ab67f2e865accc0c5e5ec242e8
SHA-1: 2ad4420915c8a44a7bd24f9ab34dac7a885089f4
RIPEMD-160: 0641797440a3920eb8ddb6ff10ae73f6d00ef9ad
PGP Signature: service-update-dyndns-run.asc

Configure the pipe-watcher script

The pipe-watcher script needs to be modified to watch a different named pipe, and run a different script when it receives input on that pipe. We also need to tell it that the data we receive on the pipe should be given to that script on its "standard in" channel.

# nano pipe-watcher Use whatever text editor you like. Line numbers refer to the 2006-06-26 version of the script (which is still current as of 2008-05-26.)
Find this line (line 35)...
my $pfile     = "/tmp/update-qmail" ;

Change it to say...
my $pfile     = "/tmp/update-dyndns" ;

Find this line (line 38)...
my $cmd     = "/service/qmail-updater/update-qmail" ;

Change it to say...
my $cmd     = "/service/update-dyndns/update-dyndns" ;
Find this line (line 39)...
my $cmd_needs_data   = 0 ;

Change it to say...
my $cmd_needs_data   = 1 ;

Set ownership of the data directory

When the service receives a valid change, it will write a text file in the "data" directory. In order to do this, either the files must already exist and be writeable to the userid as which the service runs, or the directory itself should be writeable to that userid. The easiest way to set this up is to just make the directory "owned by" that userid.

The "service-update-dyndns-run" script on the web site assumes you will be running the service as the dnsrun user. If this is not the case, substitute whatever userid (and that user's primary login group) in this command.

# chown dnsrun:dnsrun data

If you are running the service as a userid other than dnsrun, you should also edit the "run" script.

# nano run Use whatever text editor you like
Find this line (line 3)...
exec setuidgid dnsrun ./pipe-watcher 2>&1

Change the userid...
exec setuidgid userid ./pipe-watcher 2>&1

Setting up hostnames, ttl values, and keys

The last, and possibly the most important part of this, is to configure the update-tyndns script with the list of hostnames you wish to allow to be updated, along with the TTL value and key for each hostname.

To do this, you need to create a file called "hostkeys" in the service directory. The file format is simple- each line consists of the hostname, the TTL value, and the key, all separated by whitespace (i.e. spaces or tabs.) Empty lines, and lines which begin with "#", are ignored. A sample file might look like this:

# workstation at home
home.domain.xyz     300   k3nnw0rt

# laptop, wherever it may be
laptop.domain.xyz    60   some other key

Note that the key on the second record contains spaces, and will need to be sent by that target machine with spaces (which means the key will need to be quoted in the shell script- we'll get to that below.) However, because of how the script works, the keys cannot begin with, or end with, whitespace.

You should also be careful about using non-ASCII characters in the keys, unless you know that both the server's Perl implementation, and the shell on the target machine, support unicode correctly.

Of course, the hostnames you use must comply with the rules laid out in RFC 1034, and subsequently revised in RFC 1123 and RFC 2181, which basically boil down to the following:

The hostnames, obviously, need to exist within domains for which your server's tinydns service is authoritative. The keys can be anything you like, however keep in mind you will need to put it into a shell script, so you probably don't want to include any non-ASCII characters (unless your server's Perl and the target machine's shell both support unicode natively) or characters which will cause quoting issues in the shell script (i.e. quotes or backslashes.)

Note that once this is all up and running, if you need to add or remove hostnames or change their keys, you will be editing this file.

Start the service

Once everything is ready, start the service.

# ln -s /var/service/update-dyndns /service/

Wait about ten seconds, then make sure it's running.

# svstat /service/update-dyndns /service/update-dyndns/log
/service/update-dyndns: up (pid 18987) 8 seconds
/service/update-dyndns/log: up (pid 18989) 8 seconds


The ip.cgi web script

This is a CGI script which reads a hostname and key from the submitted data (either a GET or a POST query) and sends these data, along with the client's IP address, to the named pipe which the "update-dyndns" service is monitoring.

On my own server I call this "ip.cgi", however you may want or need to give it some other name. The downloadable file below has the name "ip.txt" because if it were named "ip.cgi", the server would try to run it instead of allowing you to download it. When you download it, make sure to rename it to something your server will recognize as an executable script.

File: ip.txt
Size: 1,826 bytes
Date: 2008-05-27 02:07:06 +0000
MD5: 8cf6443a114ccf98a150995ef29b66aa
SHA-1: 34f0a9198e468c5583f753fc52546b7b0bcb2002
RIPEMD-160: 193be430b12c8952c248012e27818a25f3c5ec95
PGP Signature: ip.txt.asc

The script is configured with "/tmp/update-dyndns" as the name of the named pipe to which it will write its reports. If you are using some other name (in the "pipe-watcher" script, above) then you will need to edit this script so it sends its output to the right place.


The send-dyndns script

This is a script which runs on the target system. It sends an HTTP request to the URL where you installed the ip.cgi script above, with the hostname and key associated with that machine.

On my own laptop (running Mac OS X) I have it run once a minute, however you may want to have it run every 5, 10, 15, 30, or 60 minutes, or you may not want it to run at all, and only run it manually whenever you feel the IP address needs to be updated.

I normally use wget for command-line HTTP requests, however in this instance I used curl instead, because (1) it was already present on my Mac systems, and (2) it's much easier to specify form variables to be sent with the request using curl's "-d" options, than it is to use wget's "--post-data=" and/or "--post-file=" options.

The script is short enough to just include it on the page, rather than making it a separate download. It looks like this:

#!/bin/sh
curl -k -s -f -o - -d name=home.domain.xyz -d key=k3nnw0rt \
        https://secure.domain.xyz/ip.cgi > /dev/null

The three items you will need to set are, obviously, the hostname, the key, and the URL of the web script.

Note that this script does not need to run as root on the target machine- it can run as any userid, as long as the user hasn't been denied permission to run cron scripts (see your system's documentation.)


Testing

Once this is done, you can test the overall system by running the script. If it's working correctly, you should see a file appear in your /service/update-dyndns/data directory whose name is the hostname, which contains the tinydns data line pointing that name to the IP address of the target machine. If not, go back and figure out what isn't happening correctly- DO NOT continue unless it's working so for.

On the target machine...

$ ./send-dyndns

Then, on the server...

# cat /service/update-dyndns/data/home.domain.xyz
=hostname.domain.xyz:123.45.67.89:60


Make tinydns use the dynamic data

This is where it starts to get tricky. There is no easy download for this part; you need to understand how "make" and "tinydns-data" work in order to properly edit the "Makefile" for your tinydns service. You should be in your tinydns service's "root" directory for most of what follows:

# cd /service/tinydns/root You may have some other name for the service- for example, on my own server the service name is "a.ns.jms1.net".

As detailed in djb's documentation, the tinydns-data program reads its data lines from a file called "data", and creates a file called "data.cdb", both in the current directory. It may create other files whose names begin with "data" while it's working.

The Makefile which tinydns-conf creates when you first set up the service looks like this:

data.cdb: data
        /usr/local/bin/tinydns-data

What this means... the first line says that "data.cdb" is a potential build target (i.e. a file which "make" may have to build or rebuild.) It depends on a file called "data", which means that if the data file is NEWER than data.cdb, it should build a new data.cdb file.

The second line tells make what command to run to actually build data.cdb. This is a very simple block; in a typical Makefile (including what we'll be adding below) you will see more than one command. The "make" program distinguishes a target from a build instruction by requiring build instructions to start with whitespace (i.e. spaces or tabs.)

Building "data" from multiple sources

This by itself is fine, if editing the data file is the only way you update your DNS data. However in this case, we need to add a new step to the system- one which builds the data file based on the contents of other files, and then those other files would be the ones you edit when you need to update your DNS data.

On my own system, I have files with names like "zones.jms1", "zones.client1", "zones.client2", and so forth... and my Makefile looks something like this:

data.cdb: data
        /usr/local/bin/tinydns-data

data: zones.jms1 zones.client1 zones.client2
        cat $^ > $@

The first part is the same. I've added a second block which says that the "data" file is also a potential build target, and that it relies on the listed zones.blah files- which means that if ANY of the zones.blah files are newer than data, it will run the commands below to rebuild the data file.

The command itself (cat) should be familiar, however the "$^" and "$@" variables are probably new (unless you're used to dealing with Makefiles already.) The "$^" variable expands to a list of the dependency files- in this case, "zones.jms1 zones.client1 zones.client2". The "$@" variable expands to the name of the current target, in this case "data".

So what it's actually doing is using cat to concatenate (i.e. "add, one after the other") all of the dependency files together into one single file called "data".

When you run make without a specific target (i.e. "make data.cdb") it builds the first target in the Makefile. This is why I added the new block to the end of Makefile rather than the beginning.

The make program is smart enough to know that it needs to build the data file before building the data.cdb file... so if you change one of the zones.blah files and run make, you will see it run the cat command to rebuild data, and then run tinydns-data to rebuild data.cdb.

# touch zones.client2
# make
cat zones.jms1 zones.client1 zones.client2 > data
/usr/local/bin/tinydns-data

Bringing in the dyndns data

Now that we've covered the concept of building the data file from multiple sources, we need to add one more thing to the mix- we need to link the files created by the update-dyndns script into the tinydns root directory, and then change our Makefile so it uses them as dependencies of the data file.

# cd /service/tinydns/root
# ln -s /service/update-dyndns/data/home.domain.xyz dyn.home.domain.xyz
# ln -s /service/update-dyndns/data/laptop.domain.xyz dyn.laptop.domain.xyz

The next step is to rename your current data file to something else, so it doesn't accidentally get overwritten. When I first did this, I used the name "zones.jms1", but you can use pretty much whatever name you like- so long as you understand it. (Note: don't touch the data.cdb file, that's what your tinydns service is actually using.)

# mv data zones.data

Now the important part - adding the lines to your Makefile which will make it build the "data" file. If you're using the filenames shown in the examples here, your Makefile will look like:

data.cdb: data
        /usr/local/bin/tinydns-data

data: zones.data dyn.home.domain.xyz dyn.laptop.domain.xyz
        cat $^ > $@

Once you've made the change, you can type "make data" to build a data file without changing data.cdb. If you do this, the data file it builds should contain the contents of all three files, one after the other.

When you're satisfied that that's working, you can type "make" to build the data.cdb file which contains your normal data plus the dynamic name(s).


The "trigger-make" program

The last piece of the puzzle is to create a way for the update-dyndns service to update the tinydns data whenever one of the dynamic names changes. The simplest way would be to just "cd" into the directory and run "make", however the update-dyndns service runs as a non-root user, so it doesn't have permissions to do that.

The solution to this is a program called "trigger-make", which I wrote last year while writing a web page explaining how to replicate tinydns data from one server to another. This program does exactly what the update-dyndns program can't do- it cd's to a specific directory, and runs "make".

# cd /service/tinydns/root
# wget http://qmail.jms1.net/djbdns/trigger-make.c
...

If your tinydns service isn't "/service/tinydns", you will need to edit the source code before compiling it.

Find this line (line 30)...
#define DIR  "/service/tinydns/root"

... and change it as needed.
#define DIR  "/service/something/root"

If your make executable isn't "/usr/bin/make", or if you need to run a different program (like gmake), you will need to edit the source code before compiling it.

Find this line (line 31)...
#define MAKE "/usr/bin/make"

... and change it as needed.
#define MAKE "/usr/bin/gmake"

After making any necessary changes, compile the program and set the permissions on the resulting executable so it runs as root, and is executable by the dnsrun user.

# gcc -o trigger-make trigger-make.c
# chown root:dnsrun trigger-make This is the group ID of the userid as which the update-dyndns service runs.
# chmod 4710 trigger-make

After creating this program, you should be able to do a full test by deleting the /service/update-dyndns/data file for one of your dynamic names, watching the log file for the update-dyndns service (using "tail -F" or something similar), and then running the send-dyndns script on that target. You should see the file be created again, and you should see the data.cdb file be rebuilt.