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.
#!/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