#!/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:InternetGatewayDevice: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->getfriendlyname, $ip_address; # XXX Do a name lookup and do an early exit if we don't need to update if( $current_address eq $ip_address ) { if( $force ) { status "Current IP address and address in DNS are identical, but --force is in effect"; } else { status "Current IP address and address in DNS are identical, 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 important 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 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 on both machines. =cut