I'm using DynDNS.org, but recently they got more obnoxious with their (free!) service, and being a cheapskate who doesn't want to be annoyed, I set up my own dynamic DNS update with my vhost at Hosteurope. I have set up ns.example.com as the DNS server for the dyn.example.com zone with the Hosteurope DNS and run my own DNS server on my vhost to serve entries for *.dyn.example.com. The key setup is done according to the many dynamic DNS articles.
The below program reads the external IP address from my UPnP enabled gateway, a FritzBox. It then sends the signed DNS update packet
to my DNS server.
The setup is not particularly secure, as the key can be used to update any dynamic IP address in the dyn.example.com zone. But for my purpose of having one name map to a dynamic IP address, that's just enough.
#!/usr/bin/perl -w
use strict;
use vars qw($VERSION);
use Net::UPnP::ControlPoint;
use Net::UPnP::GW::Gateway;
use Net::DNS '0.74'; # Earlier versions had bugs in the TSIG handling
use Getopt::Long;
use Pod::Usage;
$VERSION= '0.01';
GetOptions(
'v|verbose' => \my $verbose,
'k|key:s' => \my $key,
'n|key-name:s' => \my $key_name,
'f|key-file:s' => \my $keyfile,
's|server:s' => \my $server,
'h|hostname:s' => \my $hostname,
'z|zone:s' => \my $zone,
't|ttl:s' => \my $ttl,
'force' => \my $force,
'help' => \my $help,
'man' => \my $man,
) or pod2usage(2);
pod2usage(1) if $help;
pod2usage(-verbose => 2) if $man;
pod2usage("$0: No hostname to update." unless $hostname;
sub status {
print "@_\n"
if $verbose;
};
$ttl||= 600;
if( ! $zone) {
($zone=$hostname)=~ s!^\w+\.!!;
status("Assuming DNS zone is $zone");
};
my $upnp= Net::UPnP::ControlPoint->new();
my $current_address= '';
my $resolver= Net::DNS::Resolver->new();
# Find the relevant nameserver for the zone
if( ! $server ) {
my $query= $resolver->search($zone,'NS');
if( $query ) {
for my $rr (grep { 'NS' eq $_->type } ($query->answer)) {
$server= $rr->nsdname;
status "Nameserver for zone '$zone' is '$server'.";
};
} else {
die "Couldn't find a nameserver for zone '$zone'.\n";
};
};
$resolver->nameservers($server);
status "Looking up IP for '$hostname'";
my $query= $resolver->search($hostname);
if(! $query) {
my $err= $resolver->errorstring();
if( 'NXDOMAIN' ne $err ) {
die "Lookup of '$hostname' failed: $err\n";
} else {
status "Hostname '$hostname' was not found.";
};
} else {
for my $rr (grep { 'A' eq $_->type } ($query->answer)) {
$current_address= $rr->address;
status "Hostname '$hostname' resolves to IP address '$current_
+address'";
};
};
status "Searching local network for UPnP-enabled gateways";
my @devices= $upnp->search( st => 'urn:schemas-upnp-org:device:Interne
+tGatewayDevice:1', mx => 3 );
foreach my $dev (@devices) {
my $type= $dev->getdevicetype;
my $gw= Net::UPnP::GW::Gateway->new;
$gw->setdevice( $dev );
my $ip_address= $gw->getexternalipaddress;
status sprintf "Gateway '%s' has IP address %s", $dev->getfriendly
+name, $ip_address;
# XXX Do a name lookup and do an early exit if we don't need to up
+date
if( $current_address eq $ip_address ) {
if( $force ) {
status "Current IP address and address in DNS are identica
+l, but --force is in effect";
} else {
status "Current IP address and address in DNS are identica
+l, skipping update";
exit 0;
};
};
my $update = Net::DNS::Update->new($zone);
$update->push( update => rr_del("$hostname. A") );
$update->push( update => rr_add("$hostname. 600 A $ip_address") );
if( $keyfile ) {
status "Signing from key file $keyfile";
$update->sign_tsig( $keyfile );
} else {
status "Signing from command line with key named $key_name";
$update->sign_tsig( $key_name, $key );
};
status "Updating IP for $hostname on $server to $ip_address";
status $update->string if $verbose;
my $reply= $resolver->send( $update );
if( $reply ) {
if( 'NOERROR' eq $reply->header->rcode) {
status "Success";
} else {
die "Update failed: " . $reply->header->rcode . "\n";
};
} else {
die "Update failed: " . $resolver->errorstring . "\n";
};
};
__END__
=head1 NAME
update-wan-ip - dynamic DNS update from an UPnP enabled gateway
=head1 SYNOPSIS
update-wan-ip [options]
update-wan-ip -h HOSTNAME -k KEYFILE
update-wan-ip -h home.dyn.example.com -k /root/Kdyn-example-com+157+
+12345.key
Options:
--hostname, -h
--key-file, -f
--key, -k
--key-name, -n
--force
--verbose
--help
--man
=head1 OPTIONS
=over 4
=item B<--hostname>, B<-h>
--hostname home.dyn.example.com
Set the hostname you want to update.
=item B<--key-file>, B<-k>
--key-file /root/Kdyn-example-com+157+12345.key
Name of the keyfile that contains the key for the DNS updates.
Note that the filename also carries the name of the DNS key. This is i
+mportant
and must match the name of the DNS key in your DNS server.
=item B<--key-name>, B<-n>
--key-name dyn-example-com
Name of the key that is used for the DNS update.
If you don't use a keyfile, for example while testing the setup,
you can specify the name of the key through this switch. This
does not override a name given through a keyfile.
=item B<--key>, B<-k>
--key AAANN3...==
The key that is used for the DNS update.
If you don't use a keyfile, for example while testing the setup,
you can specify the key through this switch.
=back
=head1 TROUBLESHOOTING
Test with the C<nsupdate> command whether
DNS updates work at all:
nsupdate -y key-name:key
server ns.example.com
zone dyn.example.com
update add home.dyn.example.com 600 A 127.0.0.1
show
send
Check that both machines have the same time. If not, install C<ntp>
on both machines.
=cut