The problem with external dynamic DNS services like dyndns.org, no-ip.com, etc. is
that you constantly have to look after them. Either they are free, but they
expire after 1 month and you have to go to their web site to re-activate
your account. Or you pay for them, but then you need to take care of the
payments, update the credit card info, etc. This is all much too cumbersome for
something that should be entirely automated.
If you manage your own DNS anyway, it may be simpler in the long run to set-up your
own dynamic DNS system.
Bind has everything needed. There is a lot of info on the Internet on how to
do it, but what I found tended to be more complicated than becessary or
insecure or both. So here is how I did it on a Debian 6 ("wheezy") server.
The steps described below are:
Initialize variables
To make it easier to copy/paste commands, we initialize a few variables
binddir="/var/cache/bind"
etcdir="/etc/bind"
(In Debian, you can use grep directory
/etc/bind/named.conf.options to find the correct binddir value)
For dynamic hosts, we will use a subdomain of our main zone:
.dyn.example.com.
host=myhost; zone=dyn.example.com
Create key
Most example use the dnssec-keygen command. That would create 2
files (with ugly names): one .private and one .key
(public) file. This is useless since the secret key is the same in both files,
and the nsupdate method doesn't use a public/private key mechanism
anyway.
There is a less-known and more appropriate command in recent distributions :
ddns-confgen. By default, it will just print sample entries with
instructions to STDOUT. You can try it out with:
ddns-confgen -r /dev/urandom -s $host.$zone.
The options we use here are to use an "hmac-md5" algorithm instead of the
default "hmac-sha256". It simplifies things with nsupdate later.
And we also specify the key name to be the same as the host's name. That way,
we can use a wildcard in the "update-policy" in named.conf.local
and don't need to update it every time we add a host.
ddns-confgen -r /dev/urandom -q -a hmac-md5 -k $host.$zone -s $host.$zone. | tee -a $etcdir/$zone.keys
chown root:bind $etcdir/$zone.keys
chmod u=rw,g=r,o= $etcdir/$zone.keys
Depending on how you intend to use nsupdate, you may want to
also have a separate key file for every host key. nsupdate cannot
use the $zone.keys file if it contains multiple keys. So you might prefer to
directly create these individual keyfiles by adding something like >
$etcdir/key.$host.$zone :
ddns-confgen -r /dev/urandom -q -a hmac-md5 -k $host.$zone -s $host.$zone. | tee -a $etcdir/$zone.keys > $etcdir/key.$host.$zone
chown root:bind $zone.keys $etcdir/key.*
chmod u=rw,g=r,o= $zone.keys $etcdir/key.*
Configure bind
Create zone file
Edit $binddir/dyn.example.com :
$ORIGIN .
$TTL 3600 ; 1 hour
dyn.example.com IN SOA dns-server.example.com. hostmaster.example.com. (
1 ; serial (start at 1 for a dynamic zone instead of the usual date-based serial)
3600 ; refresh by secondaries (but they get NOTIFY-ed anyway)
600 ; retry (every 10 minutes if refresh fails)
604800 ; expire (slaves remove the record after 1 week if they could not refresh it)
300 ; minimum ttl for negative answers (5 minutes)
)
NS dns-server.example.com.
$ORIGIN dyn.example.com.
Edit /etc/bind/named.conf.local
Edit /etc/bind/named.conf.local to add :
// DDNS keys
include "/etc/bind/dyn.example.com.keys";
// Dynamic zone
zone "dyn.example.com" {
type master;
file "/var/cache/bind/dyn.example.com";
update-policy {
// allow host to update themselves with a key having their own name
grant *.dyn.example.com self dyn.example.com.;
};
};
Reload server config
rndc reload && sleep 3 && grep named /var/log/daemon.log | tail -20
(adjust the sleep and tail values depending on the number of zones your DNS
server handles, so that it has time to report any problems)
Test
If you created individual key files, or your $zone.keys file contains only a
single key, you can test like this:
host=myhost; ip=10.11.12.13; zone=dyn.example.com; server=dns-server.example.com; keyfile=$etcdir/key.$host.$zone
echo -e "server $server\n zone $zone.\n update delete $host.$zone.\n update
add $host.$zone. 60 A $ip\n send" | nsupdate -k "$keyfile"
Or, more readable and with an extra TXT record:
cat <<EOF | nsupdate -k $keyfile
server $server
zone $zone.
update delete $host.$zone.
update add $host.$zone. 60 A $ip
update add $host.$zone. 60 TXT "Updated on $(date)"
send
EOF
(If you get a could not read key from $keyfile: file not found error, and the file actually exists and is owned by the bind process user, you may be using an older version of nsupdate (like the version in Debian Etch). In that case, replace nsupdate -k $keyfile with nsupdate -y "$key_name:$secret" using the key name and secret found in your key file.)
Check the result:
host -t ANY $host.$zone
It should output something like
myhost.dyn.myhost.dyn.example.com descriptive text "Update on Tue Jan 1 17:16:03 CET 2013"
example.com has address 10.11.12.13
If you try to use a file with multiple keys in the -k option to nsupdate, you
will get an error like this:
... 'key' redefined near 'key'
could not read key from FILENAME.keys.{private,key}: already exists
Usage
In a /etc/network/if-up.d/ddnsupdate script.
If you have setup an update CGI page on your server, you could use
something like this, letting the web server use the IP address it received
anyway with your request.
#!/bin/sh
server=dns-server.example.com
host=myhost
secret="xBa2pz6ZCGQJ5obmvmp26w==" # copy the right key from $etcdir/$zone.keys
wget -O /dev/null --no-check-certificate
"https://$server/ddns/update.cgi?host=$host;secret=$secret"
Otherwise, you can use nsupdate, but you need to determine your external IP
first :
#!/bin/sh
server=dns-server.example.com
zone=dyn.example.com
host=myhost
secret="xBa2pz6ZCGQJ5obmvmp26w==" # copy the right key from $etcdir/$zone.keys
ip=$(wget -q -O - http://alma.ch/myip.cgi)
cat <<EOF | nsupdate
server $server
zone $zone.
key $host.$zone $secret
update delete $host.$zone.
update add $host.$zone. 60 A $ip
update add $host.$zone. 60 TXT "Updated on $(date)"
send
EOF
An improvement on these minimal scripts might be to store the IP in a file, and only do the update if needed
Web server update.cgi
An example update.cgi :
#!/usr/bin/perl
## Use nsupdate to update a DDNS zone.
## (This could be done with the Net::DNS module. It
## would be more portable (Windows, etc.), but also
## more complicated. So I chose the nsupdate utility
## that comes with Bind instead.)
# "mi\x40alma.ch", 2013
use strict;
my $VERSION = 0.2;
my $debug = 1;
my $title = "DDNS update";
my $zone = "dyn.example.com";
my $server = "localhost";
my $nsupdate = "/usr/bin/nsupdate";
use CGI qw(:standard);
my $q = new CGI;
my $CR = "\r\n";
print $q->header(),
$q->start_html(-title => $title),
$q->h1($title);
if (param("debug")) {
$debug = 1;
};
my $host = param("host");
my $secret = param("secret");
my $ip = param("ip") || $ENV{"REMOTE_ADDR"};
my $time = localtime(time);
foreach ($host, $secret, $ip) {
s/[^A-Za-z0-9\.\/\+=]//g; # sanitize, just in case...
unless (length($_)) {
die "Missing or bad parameters. host='$host', secret='$secret', ip='$ip'\n";
}
}
my $commands = qq{
server $server
zone $zone.
key $host.$zone $secret
update delete $host.$zone.
update add $host.$zone. 60 A $ip
update add $host.$zone. 60 TXT "Updated by $0 v. $VERSION, $time"
send
};
print $q->p("sending update commands to $nsupdate:"), $CR,
$q->pre($commands), $CR;
open( NSUPDATE, "| $nsupdate" ) or die "Cannot open pipe to $nsupdate : $!\n";
print NSUPDATE $commands or die "Error writing to $nsupdate : $!\n";
close NSUPDATE or die "Error closing $nsupdate : $!\n";
print $q->p("Done:"), $CR;
my @result = `host -t ANY $host.$zone`;
foreach (@result) {
print $q->pre($_), $CR;
}
if ($debug) {
# also log received parameters
my @lines;
for my $key (param) {
my @values = param($key);
push @lines, "$key=" . join(", ", @values);
}
warn join("; ", @lines), "\n";
}
print $q->end_html, $CR;
__END__
Labels: code, computers, debian, dns, en, Internet, linux, networking, sysadmin