Skip to content

Run your own Dynamic DNS server

Dyn, the company behind the widely known dynamic DNS service DynDNS, will shut down it's free service effectively May 7th, 2014. Of course there are plenty of other free or freemium services out there, but history will repeat itself and these services will vanish over time, or change their business modell.

A while ago I spent an afternoon to implement my own dynamic DNS service.


What's required:


  • A DNS server which understands updates, I'm using Bind 9 here
  • A domain (just "domain" in this blog post)
  • Optional: a webserver (for the "what is my IP" service)


For my case, I created an "updates" subdomain in my domain, and pointed it to my DNS server, from /etc/bind/named.conf.local:

zone "updates.mydomain" {
        type master;
        file "/etc/bind/";

In the zone for "domain", I created a new origin entry and pointed it back to my nameserver:

; dynamic ip addresses
$ORIGIN updates.domain.
@       IN      NS      my.nameserver.

Finally, the zone file for the "updates" subdomain:

$TTL 86400
$ORIGIN updates.domain.
@               IN      SOA     my.nameserver.        postmaster.domain. (
                                2013090907   ; Serial
                                2h           ; Refresh
                                15M          ; Retry
                                604800       ; Expire
                                2h    )      ; Minimum

                        IN      NS      my.nameserver.

; dynamic ip addresses
$ORIGIN entry1.updates.domain.
@       IN      NS      my.nameserver.

$ORIGIN entry2.updates.domain.
@       IN      NS      my.nameserver.

I define two entries for host "entry1" and "entry2", and point them back to my nameserver. The reason for this: I can configure extra keys for each zone, to allow proper authentication and encryption. From /etc/bind/named.conf.local:

zone "entry1.updates.domain" {
        type master;
        file "/etc/bind/dynamic-updates/";
        allow-transfer {
                key "entry1-transfer";
        allow-update {
                key "entry1-transfer";

Please move the zone into a separate file, this file and the directory must be writable by bind (hence I moved these zones into a separate directory: /etc/bind/dynamic-updates). The initial zone for "entry1":

$TTL 86400      ; 1 day
entry1.updates.domain IN SOA my.nameserver. postmaster.domain. (
                                2013091050 ; serial
                                7200       ; refresh (2 hours)
                                900        ; retry (15 minutes)
                                604800     ; expire (1 week)
                                7200       ; minimum (2 hours)
                        NS      my.nameserver.
$TTL 60 ; 1 minute

Once you submit updates to this zone, bind will write these changes into a ".jnl" file in the same directory.

Ok, what about the "entry1-transfer" key? I define this key in /etc/bind/entry1-transfer.key:

key "entry1-transfer" {
algorithm       hmac-md5;
secret          "put secret key here";

And the line is included in /etc/bind/named.conf.local:

include "/etc/bind/entry1-transfer.key";

The key can be created using "dnssec-keygen":

dnssec-keygen -a HMAC-MD5 -b 512 -n USER entry.updates.domain

This creates two files in the current directory, the filenames are a bit cryptic, but one file ends on ".key" (contains the public key), the other one with ".private" (contains the private key). For HMAC-MD5 the public and the private key are the same.


Ok, enough server configuration - what about the client?

The client must use the same key - that's the reason why I generate different keys for each client, and let every client only authenticate it's own subdomain. For clients without "nsupdate" see this posting. To make things more easy and exchangeable I wrote a small Perl script ( which can use the very same key file as input, and update the zone on the DNS server:


use strict;
use FileHandle;
use Net::DNS;
use Data::Dumper;
use Data::Validate::IP qw(is_ipv4 is_ipv6);
use IPC::Open3;
use Symbol 'gensym';

# first parameter is the key file
if (!defined($ARGV[0])) {
my $key = extract_key($ARGV[0]);
if (!$key or $key !~ /.+:.+/) {
    print STDERR "Cannot extract key from keyfile ...\n";
$key =~ s/:/ /;

# second parameter is the hostname which shall be updated
if (!defined($ARGV[1])) {
my $hostname = $ARGV[1];
if ($hostname !~ /^[a-zA-Z0-9\.\-]+$/) {
    print STDERR "Invalid hostname ...\n";
if ($hostname =~ /\.$/) {
    print STDERR "Hostname must not end with a dot ...\n";

my $resolver = Net::DNS::Resolver->new([recurse => 1]);
my $packet = $resolver->send("$hostname", 'NS');
if (!$packet) {
    print STDERR "Error resolving hostname ...\n";
if ($packet->header->ancount == 0) {
    print STDERR "Cannot find nameserver for hostname ...\n";
my @nameserver = $packet->answer;
if (scalar(@nameserver) == 0) {
    print STDERR "Cannot find nameserver for hostname ...\n";
my $nameserver = shift(@nameserver);
$nameserver = $nameserver->string;
if ($nameserver =~ /[\d]+[\s\t]+IN[\s\t]+NS[\s\t]+([a-zA-Z0-9\.\-]+)/) {
    $nameserver = $1;
    #print "nameserver: " . $nameserver . "\n";
} else {
    print STDERR "Cannot find nameserver for hostname ...\n";

# third parameter is the IP address
# todo: handle IPv4 and IPv6 address
if (!defined($ARGV[2])) {
my $ip = $ARGV[2];
if (!is_ipv4($ip) and !is_ipv6($ip)) {
    print STDERR "Not a valid IP address ...\n";

# finally call the nsupdate program
my ($infh, $outfh, $pid);
my $err = gensym;
eval {
    $pid = open3($infh, $outfh, $err, 'nsupdate');
if ($@) {
    print STDERR "Error executing 'nsupdate' ...\n";
#print "pid: $pid\n";
print $infh "server $nameserver\n";
print $infh "key $key\n";
print $infh "zone $hostname\n";
print $infh "update delete $hostname.\n";
print $infh "update add $hostname 60 IN A $ip\n";
print $infh "send\n";

my $exit_status = $? >> 8;
if ($exit_status != 0) {
    print STDERR "Update failed ...\n";
print "Update OK\n";

sub extract_key {
    my $file = shift;

    my $fh = new FileHandle;
    if (!open($fh, "<", $file)) {
        print STDERR "error: $!\n";
        return '';
    my @content = <$fh>;

    my $name = '';
    my $key = '';

    my $content = join("\n", @content);
    if ($content =~ /key.+?\"(.+?)\".+?secret.+?\"(.+?)\".+server/s) {
        # looks like a file with a komplete bind definition, extract key
        $name = $1;
        $key = $2;

    #print "$name:$key\n";
    return $name . ':' . $key;

sub help {
    print "\n";
    print "Update Dynamic DNS record\n";
    print "\n";
    print "\n";
    print "Usage:\n";
    print "\n";
    print " $0 <key file> <dynamic host name> <new ip address>\n";
    print "\n";

This script is called with three parameters:

  1. The key file (same file used in the DNS server)
  2. The hostname which shall be updated, as example: entry1.updates.domain
  3. The new IP address

Example: entry1-transfer.key entry1.updates.domain

To make things more easy for me, I also wrote a small script which is hosted on one of my servers, which returns the current IP address of the website visitor. My cron job for automatic updates of the zone looks as follow:

0-59/15 *    * * *   root    /root/ /etc/bind/entry1-transfer.key entry1.updates.domain `lynx -source -dump http://updates.domain/my_ip.php` > /dev/null 2> /dev/null

Voila, every 15 minutes the zone is updated (if the IP is changed).


No Trackbacks


Display comments as Linear | Threaded

issac on :

Hi Andreas, I'm from Iran. I plan to have a private DDNS server I have a domain address that is Can you help me
Comments ()

ef on :

Hi buddy :) Great Article, but i confuse about having a windows client. How to configure clients on windows base?? thanks for your time and effort
Comments ()

Add Comment

Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.
E-Mail addresses will not be displayed and will only be used for E-Mail notifications.
To leave a comment you must approve it via e-mail, which will be sent to your address after submission.
Form options