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/updates.mydomain.zone"; };
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/entry1.updates.domain.zone"; 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":
$ORIGIN . $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 A 1.2.3.4
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 (dns-update.pl) which can use the very same key file as input, and update the zone on the DNS server:
#!/usr/bin/perl 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])) { help(); exit(0); } my $key = extract_key($ARGV[0]); if (!$key or $key !~ /.+:.+/) { print STDERR "Cannot extract key from keyfile ...\n"; exit(1); } $key =~ s/:/ /; # second parameter is the hostname which shall be updated if (!defined($ARGV[1])) { help(); exit(0); } my $hostname = $ARGV[1]; if ($hostname !~ /^[a-zA-Z0-9\.\-]+$/) { print STDERR "Invalid hostname ...\n"; exit(1); } if ($hostname =~ /\.$/) { print STDERR "Hostname must not end with a dot ...\n"; exit(1); } my $resolver = Net::DNS::Resolver->new([recurse => 1]); #$resolver->debug(1); my $packet = $resolver->send("$hostname", 'NS'); if (!$packet) { print STDERR "Error resolving hostname ...\n"; exit(1); } if ($packet->header->ancount == 0) { print STDERR "Cannot find nameserver for hostname ...\n"; exit(1); } my @nameserver = $packet->answer; if (scalar(@nameserver) == 0) { print STDERR "Cannot find nameserver for hostname ...\n"; exit(1); } 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"; exit(1); } # third parameter is the IP address # todo: handle IPv4 and IPv6 address if (!defined($ARGV[2])) { help(); exit(0); } my $ip = $ARGV[2]; if (!is_ipv4($ip) and !is_ipv6($ip)) { print STDERR "Not a valid IP address ...\n"; exit(1); } # 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"; exit(1); } #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"; exit(1); } print "Update OK\n"; exit(0); sub extract_key { my $file = shift; my $fh = new FileHandle; if (!open($fh, "<", $file)) { print STDERR "error: $!\n"; return ''; } my @content = <$fh>; close($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:
- The key file (same file used in the DNS server)
- The hostname which shall be updated, as example: entry1.updates.domain
- The new IP address
Example:
dns-update.pl entry1-transfer.key entry1.updates.domain 10.0.0.1
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/dns-update.pl /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).
Comments
Display comments as Linear | Threaded
issac on :
ef on :