Summary
This how-to explains a way to build your own dynamic DNS server.
Main
Hi, since some time my DDNS provider has problems which cause the loss of the connection to my home server.
To prevent this loss I’ve read some manpages and build my own DDNS server.
To prevent this loss I’ve read some manpages and build my own DDNS server.
Image may be NSFW.
Clik here to view.
This is not a how-to about DNS, bind or any other software I’ve used. Image may be NSFW.
Clik here to view.
Clik here to view.

Clik here to view.

Warranty
There is no warranty that this how-to works for any system.
I’m just providing these information because it worked for me this way.
If you have questions you can leave a message here but I decide whether I’ll answer and help or not.
I’m just providing these information because it worked for me this way.
If you have questions you can leave a message here but I decide whether I’ll answer and help or not.
Requirements:
- own server with static IP
- own domain resolving (e.g.: example.org)
- subdomain delegated to to your server (e.g.: dyndns.example.org)
- php5
- webserver supporting PHP (I use Lighttpd, but any will do)
- bind>=9
- dnsutils
Configurations
You have to change all ‘dyndns.example.org’ to your domain.
bind
- the bind user requires write-access to bind working directory:
// named.conf
options {
...
; working directory of bind
directory "/var/named";
...
};chmod 770 /var/named/
- we generate a TSIG-key in a new directory which is used to verify the server and client:
mkdir -p /etc/named/
cd /etc/named/
dnssec-keygen -a hmac-sha512 -b 512 -n HOST dyndns.example.org
# webserver-group needs read access to file containing TSIG-key
chown root:<webserver-group> /etc/named/Kdyndns.example.org*.private
chmod 640 /etc/named/Kdyndns.example.org*.private
# get and remember the key
grep Key /etc/named/Kdyndns.example.org*.private - create zone in named.conf
key mykey {
algorithm hmac-sha512;
secret "the-generated-key";
};
zone "dyndns.example.org" IN {
type master;
file "dyndns.example.org.zone";
allow-query { any; };
allow-transfer { none; };
allow-update { key mykey; };
}; - create zone-file
- /var/named/dyndns.example.org.zone
$ORIGIN .
$TTL 86400 ; 1 day
dyndns.example.org IN SOA localhost. root.localhost. (
52 ; serial
3600 ; refresh (1 hour)
900 ; retry (15 minutes)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
NS localhost.
$ORIGIN dyndns.example.org.
Webserver
Create a subdomain (dyndns.example.org) and a vhost for the updating script.
For security purpose and compatibility of the php-script the vhost has to be protected by http-authentication.
For Lighttpd you can use the script provided here to generate the users.
Save this PHP-script in the vhost-directory:
For security purpose and compatibility of the php-script the vhost has to be protected by http-authentication.
For Lighttpd you can use the script provided here to generate the users.
Save this PHP-script in the vhost-directory:
- index.php
<?php
// configuration of user and domain
$user_domain = array( 'user' => array('subdomain','sub2'), 'user2' => array('sub4') );
// main domain for dynamic DNS
$dyndns = "dyndns.example.org";
// short sanity check for given IP
function checkip($ip)
{
$iptupel = explode(".", $ip);
foreach ($iptupel as $value)
{
if ($value < 0 || $value > 255)
return false;
}
return true;
}
// retrieve IP
$ip = $_SERVER['REMOTE_ADDR'];
// retrieve user
if ( isset($_SERVER['REMOTE_USER']) )
{
$user = $_SERVER['REMOTE_USER'];
}
else if ( isset($_SERVER['PHP_AUTH_USER']) )
{
$user = $_SERVER['PHP_AUTH_USER'];
}
else
{
syslog(LOG_WARN, "No user given by connection from $ip");
exit(0);
}
// open log session
openlog("DDNS-Provider", LOG_PID | LOG_PERROR, LOG_LOCAL0);
// check for given domain
if ( isset($_POST['DOMAIN']) )
{
$subdomain = $_POST['DOMAIN'];
}
else if ( isset($_GET['DOMAIN']) )
{
$subdomain = $_GET['DOMAIN'];
}
else
{
syslog(LOG_WARN, "User $user from $ip didn't provide any domain");
exit(0);
}
// check for needed variables
if ( isset($subdomain) && isset($ip) && isset($user) )
{
// short sanity check for given IP
if ( preg_match("/^(\d{1,3}\.){3}\d{1,3}$/", $ip) && checkip($ip) && $ip != "0.0.0.0"&& $ip != "255.255.255.255" )
{
// short sanity check for given domain
if ( preg_match("/^[\w\d-_\*\.]+$/", $subdomain) )
{
// check whether user is allowed to change domain
if ( in_array("*", $user_domain[$user]) or in_array($subdomain, $user_domain[$user]) )
{
if ( $subdomain != "-" )
$subdomain = $subdomain . '.';
else
$subdomain = '';
// shell escape all values
$subdomain = escapeshellcmd($subdomain);
$user = escapeshellcmd($user);
$ip = escapeshellcmd($ip);
// prepare command
$data = "<<EOF
zone $dyndns
update delete $subdomain$user.$dyndns A
update add $subdomain$user.$dyndns 300 A $ip
send
EOF";
// run DNS update
exec("/usr/bin/nsupdate -k /etc/named/K$dyndns*.private $data", $cmdout, $ret);
// check whether DNS update was successful
if ($ret != 0)
{
syslog(LOG_INFO, "Changing DNS for $subdomain$user.$dyndns to $ip failed with code $ret");
}
}
else
{
syslog(LOG_INFO, "Domain $subdomain is not allowed for $user from $ip");
}
}
else
{
syslog(LOG_INFO, "Domain $subdomain for $user from $ip with $subdomain was wrong");
}
}
else
{
syslog(LOG_INFO, "IP $ip for $user from $ip with $subdomain was wrong");
}
}
else
{
syslog(LOG_INFO, "DDNS change for $user from $ip with $subdomain failed because of missing values");
}
// close log session
closelog();
?>
Usage
If you’ve configured all correctly you can update domains using this command:
wget --no-check-certificate --http-user="user" --http-passwd="password" --post-data "DOMAIN=example" -q https://dyndns.example.com
Some examples:
Script configuration:
$user_domain = array( 'user' => array('subdomain') );
$dyndns = "dyndns.example.org"
Result:
The user ‘user’ can update the IP for the domain
The user ‘user’ can update the IP for the domain
subdomain.user.dyndns.example.org
.Script configuration:
$user_domain = array( 'user' => array('subdomain'), 'user2' => array('test', 'foobar') );
$dyndns = "dyndns.example.org"
Result:
The user ‘user’ can update the IP for the domain
The user ‘user2′ can update the IP for the domains
The user ‘user’ can update the IP for the domain
subdomain.user.dyndns.example.org
.The user ‘user2′ can update the IP for the domains
test.user2.dyndns.example.org
and foobar.user2.dyndns.example.org
.Script configuration:
$user_domain = array( 'user' => array('*'), 'user2' => array('test', 'foobar') );
$dyndns = "dyndns.example.org"
Result:
The user ‘user’ can update the IP for the wildcard domain
The user ‘user2′ can update the IP for the domains
The user ‘user’ can update the IP for the wildcard domain
*.user.dyndns.example.org
which means all subdomains of user.dyndns.example.org
are resolved to the IP set for *.The user ‘user2′ can update the IP for the domains
test.user2.dyndns.example.org
and foobar.user2.dyndns.example.org
.Script configuration:
$user_domain = array( 'user' => array('-','subdomain'), 'user2' => array('test', 'foobar') );
$dyndns = "dyndns.example.org"
Result:
The user ‘user’ can update the IP for the domains
The user ‘user2′ can update the IP for the domains
The user ‘user’ can update the IP for the domains
subdomain.user.dyndns.example.org
and user.dyndns.example.org
.The user ‘user2′ can update the IP for the domains
test.user2.dyndns.example.org
and foobar.user2.dyndns.example.org
.Sources
This blog post was created on 2011-01-31 at 20:06 and last modified on 2014-01-11 at 14:25 by Andrwe Lord Weber.
It is tagged with
It is tagged with
.
Comments
Image may be NSFW.
Clik here to view.![]()
Clik here to view.

Great article! I think the examples would apply to a few other providers with some very minor tweaks as well. When all else fails, you can always use a free service to try to accomplish something similar such as some of the others at http://dnslookup.me/dynamic-dns/
--------------
Set up your own Dynamic DNS
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 (“squeeze”) 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 $etcdir/$zone.keys $etcdir/key.*
chmod u=rw,g=r,o= $etcdir/$zone.keys $etcdir/key.*
Configure bind
Create zone file
Edit
$binddir/$zone
:$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)
)
$ORIGIN dyn.example.com.
NS dns-server.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. 600 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. 600 A $ip
update add $host.$zone. 600 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.example.com descriptive text "Update on Tue Jan 1 17:16:03 CET 2013"myhost.dyn.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://example.com/myip.cgi)
cat <<EOF | nsupdate
server $server
zone $zone.
key $host.$zone $secret
update delete $host.$zone.
update add $host.$zone. 600 A $ip
update add $host.$zone. 600 TXT "Updated on $(date)"
send
EOF
I used a very simple
myip.cgi
script on the web server, to avoid having to parse the output of the various existing services which show your IP in the browser:#!/bin/sh
echo "Content-type: text/plain"
echo ""
echo $REMOTE_ADDR
This alternative script example uses SNMP to get the WAN IP from the cable router. It only does the update if the address has changed, and logs to syslog.
#!/bin/sh
zone=dyn.example.com
host=myname
secret="nBlw18hxipEyMEVUmwluQx=="
router=192.168.81.3
server=$(dig +short -t SOA $zone | awk '{print $1}')
ip=$( snmpwalk -v1 -m RFC1213-MIB -c public $router ipAdEntAddr | awk '!'"/$router/ {print \$4}" )
if [ -z "$ip" ]; then
echo "Error getting wan ip from $router" 1>&2
exit 1
fi
oldip=$(dig +short $host.$zone)
if [ "$ip" == "$oldip" ]; then
logger -t `basename $0` "No IP change for $host.$zone ($ip)"
exit
fi
cat <<EOF | nsupdate
server $server
zone $zone.
key $host.$zone $secret
update delete $host.$zone.
update add $host.$zone. 600 A $ip
update add $host.$zone. 600 TXT "Updated on $(date)"
send
EOF
logger -t `basename $0` "IP for $host.$zone changed from $oldip to $ip"
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. 600 A $ip
update add $host.$zone. 600 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__