Dynamic DNS – Replacing dyndns with Bind

Written by max on July 8, 2013

Description

Now that dyndns is starting to make you either a. pay money, or b. login every month, you would like to run your own Dynamic DNS services. I implement a secure dynds-compatible server in Perl CGI, hosted by Apache.

This assumes you have your own functioning server with a fixed IP that is running DNS service for at least one domain using the Bind DNS server. My personal setup is VirtualMin running on Ubuntu, so adjust accordingly if you’re on a different Distro.

Reference

The following blogs and documents were most helpful in putting this together, thanks!

Implementation

I create one directory where I put all the key files and the htpasswd file for the CGI script.
Optionally, you can give the individual key file to a user so they can use nsupdate remotely, if they’re power users.

Create a Key For Each Host

Create a key file for each host that will become a dynamic DNS host

dnssec-keygen -a HMAC-MD5 -b 512 -n USER dynamichost.yourdomain.com.
mkdir /home/myserver/dyndns
cp K* /home/myserver/dyndns

Note that USER is a fixed keyword here, no need to change it.
You should now have two files called Kdynamichost.yourdomain.com.*.{key,private}
Copy these files to the directory you’re setting up for the control files.

Add Each Key to Each Zone

This is some manual hackery to your zone files for BIND

key dynamichost.yourdomain.com. {
    algorithm HMAC-MD5;
    secret "secret from key file here==";
};
 
zone "yourdomain.com" {
        type master;
        file "/var/lib/bind/yourdomain.com.hosts";
        allow-transfer {
                127.0.0.1;
                localnets;
                buddyns;
                rollernet;
                };
        update-policy {
                grant dynamichost.yourdomain.com. name dynamichost.yourdomain.com. A;
        };
        };

A couple notes here:

  • The *.hosts file’s contents will be clobbered by the dynamic update. This is the point.
  • I’m using a very specific permission for the key to be able to modify only one entry. Other people suggest using the more permissive ‘allow-update’ command, but this allows edits to the whole zone.

Now restart bind and check the logs

service bind9 restart
grep named /var/log/syslog | tail

Making changes to a dynamic zone

Once you need to make a manual edit to a zone file you need to “freeze” the domain temporarily so that dynamic updates don’t conflict

rndc freeze domain.com
vim /var/run/bind/domain.com.hosts
rndc thaw domain.com

Add A CGI Script

Now you need a CGI script that mimics the behavior of DYNDNS. This will allow your router to use your server without anything except changing the user/password/host.

I wrote the following one in Perl for you to use, but it requires the CGI module :

apt-get install libcgi-pm-perl

Put the following in cgi-bin/dyndns on your server’s home dir.

#!/usr/bin/perl
# (c)2013 Max Baker <max @warped.org>
# Perl Artistic License 2.0 http://opensource.org/licenses/artistic-license-2.0
#
# This is a dyndns server replacement CGI script that calls bind's nsupdate
# Reference : http://dyn.com/support/developers/api/perform-update/
 
use strict;
use CGI;
 
use vars qw/$q $hostname $myip $wildcard $mx $backmx $offline $nsupdate 
        $remote_user $user $dir %hosts /;
 
$dir = '/path/to/keyfiles/here';
$nsupdate = '/usr/bin/nsupdate';
 
%hosts = (
# Host                    user   zone         key file
'dynamichost.mydomain.com' => [ 'me','mydomain.com','Kdynamichost.mydomain.com.+157+28821.key' ],
         );
 
$q = CGI->new;
 
$hostname = $q->param('hostname');
$myip     = $q->param('myip');
$user     = $q->remote_user;
#$offline = $q->param('offline');
#$wildcard= $q->param('wildcard');
#$mx      = $q->param('mx');
#$backmx  = $q->param('backmx');
 
print $q->header();
 
# Check that we have the auth set and are sending non-blank stuff
unless (not_blank($hostname) and not_blank($myip) and not_blank($user)) {
        apachelog("not_blank");
        print "badauth\n";
        exit;
}
 
# Handle Auto-Discover of IP
if ($myip eq 'auto') {
    $myip = $q->remote_addr;
}
 
# Check the IP address makes sense
unless ($myip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/) {
        apachelog("bad_ip");
        print "badauth\n";
        exit;
}
# Multiple hosts can be given, separated by a comma
my @hosts = split(',',$hostname);
if (scalar @hosts > 10 ) {
        apachelog("too many");
        print "numhost\n";
        exit;
}
 
foreach my $host (@hosts) {
        # Check if it's a host we allow
        unless (defined $hosts{$host}) {
                apachelog("Bad host");
                print "notfqdn\n";
                last;
        }
        # Check that the user has access to this host
        unless ($hosts{$host}->[0] eq $user) {
                apachelog("Access Denied");
                print "nohost\n";
                last;
        }
        my $key = sprintf("%s/%s",$dir,$hosts{$host}->[2]);
        my $zone = $hosts{$host}->[1];
 
        unless (-r $key) {
                die "Key file $key missing.";
        }
 
        # Perform the update
        unless (open(N,"|$nsupdate -k $key 1>/dev/null")) {
                apachelog("nsupdate failed");
                print "dnserr\n";
                next;
        }
        # There should be no space between the lesser-than signs here, wordpress is adding it, remove.
        print N < < "end_update";
server $zone
zone $zone
update delete $host. A
update add $host. 86400 A $myip
show
send
end_update
        # Should have exited, otherwise we have a problem
        unless (close N) {
                apachelog("nsupdate failed on close");
                print "dnserr\n";
                next;
        }
        print "good\n";
}
 
exit;
 
sub not_blank {
        my $val = $_[0];
        return 1 if defined $val and $val !~ /^\s*$/;
        return 0;
}
 
sub apachelog {
        my $msg = join(' ',@_);
        { no warnings; 
        warn "dyndns : $user $hostname = $myip $msg\n";
        }
}

Yes, I should probably go put this on github, but I’m lazy.

Set the Apache Permissions for the CGI Script

Create a new htpasswd file. The username specified here must match the username listed in the %hosts hash above.

cd /home/myserver/dyndns
htpasswd -c dyndns.passwd me

To add additional users, leave off the -c or you will clobber the first ones!

Next edit your server’s httpd.conf file. For me this is /etc/apache2/sites-enabled/0-mydomain.conf

<location /cgi-bin/dyndns>
AuthName "My DDNS Server"
AuthType Basic
AuthUserFile /home/myserver/dyndns/dyndns.passwd
require valid-user
</location>

Be sure to add this to your https server too if you have one.

Then restart Apache

service apache2 graceful

Check your server logs in /home/myserver/logs/error_log and /var/log/apache2/error_log in case of trouble.

Use

To use this just point your router to http://www.yourdomain.com/cgi-bin/dyndns and you’re in business.

If your router does not let you select the DDNS provider then you will need to run a GET every time your IP change. You can add this to a Cron Job or Windows Scheduler. You will need wget (Available for most all platforms).

wget \
--no-check-certificate \
--http-user="me" \
--http-passwd="mepassword" \
-q -O /dev/null \
'https://www.yourdomain.com/cgi-bin/dyndns?myip=auto&hostname=dynamichost.yourdomain.com'

I put the above in to /etc/cron.daily on a host inside my network at home.

Testing

You can go there in your browser to test using a URL like this :

https://www.yourdomain.com/cgi-bin/dyndns?hostname=dynamichost.yourdomain.com&myip=1.2.3.12

You should get back a ‘good’ return code. Otherwise check the apache error_log. You should also be prompted with the normal browser authentication box or something is wrong with your apache setup.

good

Now you can check the DNS record

dig @yourdomain.com dynamichost.yourdomain.com

And you should see the new entry

;; ANSWER SECTION:
dynamichost.yourdomain.com.      86400   IN      A       1.2.3.12

Share and Enjoy!

Posted Under: Linux, Technology

22 replies to “Dynamic DNS – Replacing dyndns with Bind

  1. Max

    After all this I found that my router has the dyndns server names hard coded, no way to add a custom one.
    So I added the myip=auto feature and am using wget from a cron job on the remote side, instead of the router built-in feature.

  2. Pingback: » Secure Foscam Webcam with Audio Over httpsOne hand clapping

  3. Pingback: debian | selbstgehosteter DynDNS-Dienst | controlc.de

  4. Edward Rmepala

    I get errors – error log says
    [error] [client 1.1.1.1] Unterminated operator at /var/www/cgi-bin/dyndns line 86.
    [error] [client 1.1.1.1] Premature end of script headers: dyndns

    and on line 86 is
    print N < < "end_update";

  5. jfp

    Thinks for the great post.
    i Had the same error as #6 and found that changing
    print N < < "end_update";
    to
    print N << "end_update";
    fixed it.

    Also I had to add the following to bind config file;
    key "dynamichost.yourdomain.com." { algorithm hmac-md5; secret "SECRET_FROM_TEH_KEY_FILE";};
    You would need one per hostname unless I am missing something.
    Thanks.

  6. Post Author max

    Hi jfp, good catch thanks, I did forget to put the key info in there. It was present in my files. Also thanks for catching the typo on the << operator. Turns out wordpress is adding the space in there, so I added a comment.

  7. sasa

    Hi,is it possible to have more users ( i mean about 1000 users) in script file(.pl) … or it is better to link them to mysql database?
    I ask that because i wish to deploy cctv ddns for our users.
    Thanks

  8. Post Author max

    Hi Sasa, I would use LDAP to store the users and change the script to authenticate against the LDAP database. You could then easily reuse it for other services you might have (file shares, forums, web sites, windows, etc).

  9. Felipe Román

    you should put this section of the script:

    # There should be no space between the lesser-than signs here, wordpress is adding it, remove.
    print N << "end_update";
    server $zone
    zone $zone
    update delete $host. A
    update add $host. 400 A $myip
    show
    send
    end_update

    before the section #Perform the update

    great work!

  10. Erik

    Hi, I would suggest to change

    server $zone

    to

    server 127.0.0.1

    since the dns server might not be the same as $zone.

  11. Quentin

    I had a problem with ez-ipupdate installed on my router. It was thrown an error and return some “error processing request”. Despite the script runs properly.
    The problem is the answer of the script. It should be “good ” but with a space after good. (Source code of ez-ipupdate.c : if(strstr(buf, “\ngood “) != NULL) ).
    So I replaced :
    print “good\n”;
    with
    print “good \n”;
    And ez-ipupdate isn’t throwing an error anymore ! Thanks for you script !

  12. Per

    Hi

    First of all many thanks for this.

    I am struggeling with this error dyndns : me = not_blank

    Any idea of what I done wrong?

    Per

  13. Per

    Hi Again

    So i figured out the error in short I had filled out the
    $hostname = $q->param(‘hostname’);
    $myip = $q->param(‘myip’);
    Fields, but after editing it back to the original I now get: dyndns : me norhex.com’ = 149.11.36.130 Bad host

    Any idea what the problem is?

    Regards
    Per

  14. Jeroen van Ingen

    Hi Max, thanks for this nice blog post! I copied most of it, only I changed the script a little so it will do its own password check (no Apache auth). Works like a charm, I’m using it with the DDNS update feature of a Cisco router :-)

  15. DAvid

    hi someone can help me with this error ?

    Key file /home/myserver/dyndns/Kdude.test.com.+157+52806.key missing. at /var/cgi-bin/dyndns line 77.

    thanks

  16. Pingback: Eigener DynamicDNS (ddns) Dienst | tech-island.com

Leave a Reply

Your email address will not be published.