Dynamic DNS – Replacing dyndns with Bind

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

Źródło: Dynamic DNS – Replacing dyndns with Bind

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *