home January 01, 2017

Dynamic DNS Client Perl Script


a fast, secure and efficient ddclient DDNS replacement

Dynamic DNS (DDNS) is a method of updating a name server in the Domain Name System (DNS) with the current ip address of a DHCP enabled network interface based client as specified by RFC 2136. The most common setup is a client machine which uses the Dynamic Host Configuration Protocol (DHCP) to request an ip address from the internet service provider (ISP) like Verizon FIOS or Google FIBER. Since the DHCP server can provide any ip address in the ISP's pool, we need some way to assign the current ip of the client server to a domain name we have registered with the Dynamic DNS (DDNS) provider.

The current version of the script is designed to run on FreeBSD and used for NameCheap and FreeDNS. With some minor modifications to the $ifconfig_ip and $url variable this script could work for any dynamic dns service and any OS.

How does the script work ?

In order to update your ip address with a dynamic dns provider all you have to do is send a properly crafted https request to the DDNS server. That's it. Our perl script was only nine(9) lines long, but then we also wanted to add in error checking, comments and failure logic.

The first time you run the script it will poll the ip address of the specified interface and save the ip address. Then the script will make a connection to the NameCheap DDNS servers and register your ip address with your NameCheap registered domain name. The script will then sleep for 600 seconds. When the script wakes it will again check your current ip address. If the current ip is the same as the last known ip before the script went to sleep, then the script will sleep again. If the ip address ever changes, the script will wake after the 600 seconds sleep and call the function to update your new ip address to the register domain name.

While the script is checking the ip and sleeping there is also a forced update timer. Most dynamic dns providers expect you the check in every thirty(30) days of so to make sure your account is still active. Our script has a force update time of seven(7) days. When the seven days are up the script will force an update just as if the ip address of the interface changed.

That's about it. We have fully commented the script to make it easy for you to edit or modify as you see fit.

The dynamic dns perl script

To use the script, copy and paste the block of perl code from the following text box to a file. We are going to call the script dynamic_dns.pl for this example, but you can name it anything you like. Remember to make the file executable too, "chmod 700 dynamic_dns.pl" as the script should only be readable and executable by a trusted user.

Below this window we talk about run the script the first time in DEBUG mode.

#!/usr/bin/perl -T

use strict;
use warnings;
use Sys::Syslog qw( :DEFAULT setlogsock);
setlogsock('unix');

#
##  moneyslow.com  ,:,  Dynamic DNS client using perl and curl
##    Script Name : dynamic_dns.pl
##    Version     : 0.06
##    Valid from  : Aug 2013
##    URL Page    : https://moneyslow.com/html/webconf/dynamic_dns_ddns.html
##    OS Support  : FreeBSD, Linux, Mac OSX and OpenBSD
#                `:`

# This script is a direct replacement for ddclient. The current version of the
# script is designed to run on FreeBSD and used for NameCheap and FreeDNS. With
# some minor modifications to the $ifconfig_ip  and $url variable this script
# could work for any dynamic dns service and any OS.

############  options  ##########################################

# define your domain name which is registered with the dynamic dns server. The
# domain name string MUST be lowercase according to the ddns help page.
my $domain = "moneyslow.com";

# the dynamic DNS password retrieved from the namecheap. since the password is
# in the script it is important that only trusted users can read this file. For
# NameCheap this will be a random 32 character alpha numeric string.
my $password = "11111111aaaaaaaaa22222222bbbbbbb";

# list of all the subdomains you want to update. If you do not have any
# subdomains or are using a wild card domain then simply use empty double
# quotes. Most users just use double quotes.
# my @subdomains = ("mail","www","*");
my @subdomains = ("");

# how many seconds between network interface checks to see if our local ip has
# changed? The script is very efficient, using less then four(4) seconds of CPU
# time per month, so you could set this to as low as 10 seconds if you really
# need to. We suggest 600 seconds or 10 minutes.
my $query_ip_time = 600;

# how many seconds between forced updates even if the ip has not changed? We
# suggest seven(7) days or 604,800 seconds. Most services expect a forced
# update in no more then thirty(30) days. So, seven(7) days allows extra check
# in times in case connectivity is down. Please do not make this value too low
# so to not abuse the namecheap or freedns servers. 
my $update_time = "604800";

# what is the name of the local network interface? This interface is what
# ifconfig will query to find out what the ip is and if the ip has changed. For
# our Intel I350 card on FreeBSD the interface name is "igb0". On Ubuntu or
# Red Hat Linux you will see the external interface name as "eth0" for example.
my $nic = "igb0";

# DEBUG: use the debug option to run the script manually and watch output. Set
# to zero(0) for normal operations.
my $DEBUG = 1;

#################################################################

# clear the environment and set the minimum path.
$ENV{ENV} ="";
$ENV{PATH} = "/bin:/usr/bin:/usr/local/bin:/sbin";

# declare general worker variables
my $err_returned = 0;
my $first_run = 1;
my $ip_current = "";
my $subdomain = "";
my $update_timer = $update_time;

# The main do while loop. 
do {

  # Using ifconfig and the name of the interface we collect the ip address of
  # the local interface. The output is then untainted and cleaned up.
  # Ubuntu 13.04 syntax
  #  my $ifconfig_ip = `ifconfig $nic | grep "inet " | awk '{print $2}' | awk '{split($0,a,":"); print a[2]}'`;
  # FreeBSD 9.2 syntax
  my $ifconfig_ip = `ifconfig $nic | grep "inet " | awk '{print \$2}'`;

  The output of ifconfig is then untainted and cleaned up.
  my $ip_untainted = "$1" if ($ifconfig_ip =~ m/^([a-zA-Z0-9\.]+)$/ or die "\nError: Illegal characters in IP address\n\n" );
  chomp($ip_untainted);

 # debug option to watch variables
 if ($DEBUG) {
    print "first run?   : $first_run\n";
    print "ip current   : $ip_current\n";
    print "ip ifconfig  : $ip_untainted\n";
 }

 # if the interface ip has changed OR we need to force an update OR this is the first time
 # the script has been run then we need to force update to the dynamic dns service.
 if ( $ip_untainted ne $ip_current || $update_timer < 1 || $first_run ) {

  # for each @subdomain you defined this loop will register your current ip as
  # seen by the dynamic dns servers
  foreach $subdomain (@subdomains) {

    # This is the syntax for the secure https request to NameCheap which uses
    # FreeDNS at freedns.afraid.org . Though http will work, for a secure
    # encrypted connection please make sure https is the URI scheme. We do not
    # need to send our interface ip as the dynamic dns server will infer our ip
    # from the TCP connection.
    my $url = "https://dynamicdns.park-your-domain.com/update?host=".$subdomain."&domain=".$domain."&password=".$password;

    # The joined $url syntax from above is validated to make sure no illegal
    # characters are included.
    my $url_untainted = "$1" if ($url =~ m/^([a-zA-Z0-9\-\&\?\=\:\.\/]+)$/ or die "\nError: Illegal characters in URL\n\n" );

    # Make the request to the dynamic dns server and collect the xml output.
    # Failed connections will log the ERROR: line to syslog. Curl will make a
    # secure https (SSL) connection to the server using only TLSv1. Curl will
    # timeout the connection in 60 seconds if a response has not been recieved
    # and retry at least 3 times.
    my $response = `curl --compressed --max-time 60 --retry 3 --silent --tlsv1 "$url_untainted"`;

    # debug to watch local variables
    if ($DEBUG) {
      print "subdomain    : $subdomain\n";
      print "domain       : $domain\n";
      print "url          : $url\n";
      print "\n";
      print "response     : $response\n\n";
    }

    # When NameCheap sends its XML response back to us we look for the returned
    # ip address and error code. Using these values the script can verify the
    # DDNS update request was successful.
    my ($ip_returned)= $response =~ / <IP>(.*)<\/IP>/;
    my ($err_returned)= $response =~ /<ErrCount>(.*)<\/ErrCount>/;

    # if the ip and error code exist in the response then return success,
    # otherwise a general error is returned to syslog. Even with an error the
    # script will continue to run in case the error was transient.
    if ($ip_returned && $err_returned == 0 ) {

       # log to /var/log/messages
       &logger("SUCCESS:","$subdomain $domain bound to $ip_returned");

       # debug 
       print "SUCCESS: $subdomain $domain bound to $ip_returned \n\n" if ($DEBUG == 1);

       # update the last known cached ip so we can compare the current
       # interface ip with the last known ip to be used later in deciding if we
       # need to do a dynamic dns update
       $ip_current = $ip_untainted;

       # if the force update time has reached zero then reset the timer here
       if ( $update_timer < 1 ) {
         $update_timer = $update_time + $query_ip_time;
       }

    } else {
       # log to /var/log/messages
       &logger("ERROR:","request failed for $subdomain $domain to $ip_untainted. Check URL and connectivity.");
    }
  }

  # zero out the first run trigger
  $first_run = 0 ;

 } else {
     # debug
     print "ip the same  : update not needed.\n" if ($DEBUG == 1);
 }

  # After each loop, decrement the forced update counter by the query_ip_time
  # sleep time in seconds.
  $update_timer = $update_timer - $query_ip_time;

  # debug options
  print "forced update: $update_timer seconds\n" if ($DEBUG == 1);
  print "sleeping     : $query_ip_time seconds\n\n\n" if ($DEBUG == 1);

  # sleep time between interface ip checks
  sleep $query_ip_time;

  # if there were no errors then continue the do/while loop
} while ( $err_returned == 0 );

# global logger method to send data to /var/log/messages by way of syslogd
sub logger {
    openlog($0,'pid', 'root');
    syslog('info', "$_[0] $_[1]");
    closelog;
}

# clean exit
exit(0);

### EOF ###

How do I DEBUG the script on the first run ?

Once you have the script from the window above you need to set your registered domain name and password from NameCheap. If you need help getting the password for your dynamically updated domain name check out the NameCheap help page. Then take a look at the rest of the options. We heavily commented the script so it should be easy to understand.

The first time you run the script we suggest using DEBUG=1 mode which is set by default. This will run the script in the forground and print out all of the variables as the script sees them. Run the script on the command line and watch the output.

For our example test case we have lowered the $query_ip_time=60 seconds and $update_time=240 seconds. This is just to show you what the DEBUG output will look like for a successful update. We have further commented the output using lines indented with "###"

root@calomel:  ./dynamic_dns.pl 
first run?   : 1
ip current   : 
ip ifconfig  : 1.2.3.4
subdomain    : 
domain       : moneyslow.com
url          : https://dynamicdns.park-your-domain.com/update?host=&domain=moneyslow.com&password=11111111aaaaaaaaa22222222bbbbbbb

response     : <?xml version="1.0"?><interface-response><Command>SETDNSHOST</Command><Language>eng</Language><IP>1.2.3.4</IP><ErrCount>0</ErrCount><ResponseCount>0</ResponseCount><Done>true</Done><debug><![CDATA[]]></debug></interface-response>

SUCCESS:  moneyslow.com bound to 1.2.3.4 

forced update: 180 seconds
sleeping     : 60 seconds

             ### first run of the script forces an update.
             ### sleep for 60 seconds after initial start up.
             ### forced update in 180 seconds.
             ### /var/log/messages
             ###     Aug  9 10:00:30 calomel /tools/dynamic_dns.pl[95797]: SUCCESS:  moneyslow.com bound to 1.2.3.4

first run?   : 0
ip current   : 1.2.3.4
ip ifconfig  : 1.2.3.4
ip the same  : update not needed.
forced update: 120 seconds
sleeping     : 60 seconds

             ### the ip did not change.
             ### sleep for 60 seconds.
             ### forced update in 120 seconds.

first run?   : 0
ip current   : 1.2.3.4
ip ifconfig  : 1.2.3.4
ip the same  : update not needed.
forced update: 60 seconds
sleeping     : 60 seconds

             ### the ip did not change again.
             ### sleep for 60 seconds.
             ### forced update in 60 seconds.

first run?   : 0
ip current   : 1.2.3.4
ip ifconfig  : 1.2.3.4
ip the same  : update not needed.
forced update: 0 seconds
sleeping     : 60 seconds

             ### the ip address is still the same.
             ### sleep for 60 seconds.
             ### forced update on next run.

first run?   : 0
ip current   : 1.2.3.4
ip ifconfig  : 1.2.3.4
subdomain    : 
domain       : moneyslow.com
url          : https://dynamicdns.park-your-domain.com/update?host=&domain=moneyslow.com&password=11111111aaaaaaaaa22222222bbbbbbb

response     : <?xml version="1.0"?><interface-response><Command>SETDNSHOST</Command><Language>eng</Language><IP>1.2.3.4</IP><ErrCount>0</ErrCount><ResponseCount>0</ResponseCount><Done>true</Done><debug><![CDATA[]]></debug></interface-response>

SUCCESS:  moneyslow.com bound to 1.2.3.4 

forced update: 240 seconds
sleeping     : 60 seconds

             ### the ip did not change, but the forced update time triggered.
             ### sleep for 60 seconds.
             ### forced update in 240 seconds.
             ### /var/log/messages
             ###     Aug  9 10:03:30 calomel /tools/dynamic_dns.pl[95797]: SUCCESS:  moneyslow.com bound to 1.2.3.4

first run?   : 0
ip current   : 1.2.3.4
ip ifconfig  : 1.2.3.4
ip the same  : update not needed.
forced update: 180 seconds
sleeping     : 60 seconds

             ### the ip did not change.
             ### sleep for 60 seconds.
             ### forced update in 180 seconds.

first run?   : 0
ip current   : 5.6.7.8
ip ifconfig  : 1.2.3.4
subdomain    : 
domain       : moneyslow.com
url          : https://dynamicdns.park-your-domain.com/update?host=&domain=moneyslow.com&password=11111111aaaaaaaaa22222222bbbbbbb

response     : <?xml version="1.0"?><interface-response><Command>SETDNSHOST</Command><Language>eng</Language><IP>5.6.7.8</IP><ErrCount>0</ErrCount><ResponseCount>0</ResponseCount><Done>true</Done><debug><![CDATA[]]></debug></interface-response>

SUCCESS:  moneyslow.com bound to 5.6.7.8 

forced update: 240 seconds
sleeping     : 60 seconds

             ### network interface ip changed to 5.6.7.8 so update the dynamic dns server.
             ### sleep for 60 seconds.
             ### forced update in 240 seconds.
             ### /var/log/messages 
             ###     Aug  9 10:04:30 calomel /tools/dynamic_dns.pl[95797]: SUCCESS:  moneyslow.com bound to 5.6.7.8

...and so forth...

How do I run the script in production ?

Once you are happy with the script, set the DEBUG value to zero(0). This will make the script silent on the console and only log status messages to /var/log/messages by way of syslog. Run the script as root or any other trusted user. You can add the name of the script to /etc/rc.local for start up on boot or use the /usr/local/etc/rc.d method for FreeBSD found in the Questions section below.

HELPFUL HINT: Make FreeBSD and Ubuntu's network significantly faster by checking out our Network Speed and Performance Guide.

Questions?

How can I start the dynamic_dns.pl script on FreeBSD on boot ?

On FreeBSD we use the /usr/local/etc/rc.d method. First edit /etc/rc.conf and add the string dynamicdns_enable="YES" . Then make a new file called /usr/local/etc/rc.d/dynamicdns with the following and make sure the path to the dynamic_dns.pl script is correct for your setup. Permissions should be "chmod 555 /usr/local/etc/rc.d/dynamicdns"

#!/bin/sh

# PROVIDE: dynamicdns
# BEFORE:  LOGIN
# KEYWORD: 

. /etc/rc.subr

name=dynamicdns
rcvar=`set_rcvar`
command=/tools/dynamic_dns.pl
command_interpreter=/usr/bin/perl
dynamicdns_user=root
start_cmd="/usr/sbin/daemon -u $dynamicdns_user $command"

load_rc_config $name
run_rc_command "$1"

You can now start, stop and check the status of the dynamic_dns.pl script using the standard rc.d commands:

/usr/local/etc/rc.d/dynamicdns 
Usage: /usr/local/etc/rc.d/dynamicdns [fast|force|one|quiet](start|stop|restart|rcvar|status|poll)

/usr/local/etc/rc.d/dynamicdns stop
Stopping dynamicdns.
Waiting for PIDS: 95742.

/usr/local/etc/rc.d/dynamicdns start

/usr/local/etc/rc.d/dynamicdns status
dynamicdns is running as pid 95797.

tail -100 /var/log/messages | grep dynamic_dns
Aug  9 10:00:30 calomel /tools/dynamic_dns.pl[95797]: SUCCESS:  moneyslow.com bound to 1.2.3.4