Dynamic DNS

Last time I talked about Home Automation systems and ways to persuade those systems to perform functions they were not necessarily designed to perform. In this post, I want to talk about how one can maximize some of the built-in functionality that comes with most home automation controllers.

One of the out-of-the-box functions that many of these systems provide is remote access.  An example use of this functionality might be using your phone to turn on all the lights in your house when you drive up into the driveway.  Another example might be accessing your controller from your out-of-town hotel room to control lights, sprinklers or appliances to simulate at home behaviors, or in response to changing weather conditions. 

I know it is obvious, but this only works if you can gain network access to your controller from outside your home network.  Facilitating outside access can be accomplished in two ways.  The first is to purchase a dedicated (or sticky) IP address if your service provider offers this service.  The other option is to find a way to be router “omniscient” thus always knowing what your router’s external IP address is.

From my perspective, paying to have a dedicated IP address is a waste of money.  That leaves us with the requirement to always be aware of what our router’s external IP address is in order to consume the network services from within our home network.  Fortunately, keeping track of your router’s dynamic IP address is easier than one might expect.  The easiest way of accomplishing this, and the path we will take, is to use one of the many free dynamic dns services available on the Internet today. 

I am accomplishing my goals by leveraging two free internet services.  The first service is DynDNS and the second is DNS-O-Matic.  Why use two services you might ask ….  well, it’s complicated.  At a high-level the reasoning goes like this.  DynDNS gives me the ability to dynamically associate a URL with my router’s external interface address (our main goal), but we also use OpenDNS, for basic “content filtering”, and it needs to be aware of my IP address at all times if I want to tailor its configuration settings to my specific needs.  To automate the update process for OpenDNS, I need to use DNS-O-Matic.  Fortunately, DNS-O-Matic does help us out by providing a function to update DynDNS each time we update via its service.  So really all we have to do is figure out how to automatically update the DNS-O-Matic service.

So how does one go about updating this service!?  If you’re lucky, your router will have a Dynamic DNS configuration option that will perform this task for you, but the chance that your router will have an option for DNS-O-Matic is pretty slim.  Your other option is to look at the various software options that DNS-O-Matic provides, but, for various reasons, I didn’t like any of these options — so I wrote my own.

The reason I chose to write my own was three-fold: (1) I wanted complete control of the update intervals and conditions (2) the software had to run CLI on my Linux or Mac server (3) I wanted the ability to update a twitter feed with each DNS-O-Matic update occurrence.

There really isn’t much that cannot be accomplished with your favorite scripting language and a bit of ingenuity.  I happen to like PHP and since the PHP CLI is installed by default on newer versions of Mac OS/X (my primary server), it makes a great scripting choice.  To make this work, I wrote my script and dropped it in my /etc/periodic/daily folder and then let anacron take care of the rest.

Here’s the script:

#!/usr/bin/php -q
<?php
//Define and set baseline variables
date_default_timezone_set('America/Chicago');
$base_path = '/Users/yourusername/php-scripts/opendns/';
$fn_ip_address = $base_path . 'UVerse-IP.txt';
$fn_log = $base_path . 'opendns.log';
$fn_log2 = $base_path . 'opendns2.log';
$fn_ctr = $base_path . 'counter.txt';
$rtr_url = 'http://192.168.5.5/xslt?PAGE=B01';
$dns_uid = 'userid';
$dns_passwd = 'password';
$dns_host = 'hostname';
$twitterusername = 'twitterid';
$twitterpassword = 'twitterpass';
$ip_regex = '/([0-9]{1,3}).([0-9]{1,3}).([0-9]{1,3}).([0-9]{1,3})/';
//Let's display the current date and time
echo date("F j, Y, g:i a") . "n";
//Let's grab the page content off of the router
$page_data = get_data($rtr_url);
//Let's parse the data from our router page and look for the IP address
preg_match($ip_regex,$page_data,$matches);
$ip_match = $matches[1] . "." . $matches[2] . "." . $matches[3] . "." . $matches[4];
echo " Router: " . $ip_match . "n";
//Let's go ahead and build our DNSOMatic Updater URL
$dns_url = "https://" . $dns_uid . ":" . $dns_passwd . "@updates.dnsomatic.com/nic/update?hostname=" . $dns_hst . "&myip=" . $ip_match . "&wildcard=NOCHG&mx=NOCHG&backmx=NOCHG";
//Let's check to see if our counter file exists. If it does, let's open the file
//read its contents and then close the file.
if (file_exists($fn_ctr)) {
$fh = fopen($fn_ctr, 'r') or die("can't open counter file for reading");
$counter = fread($fh, filesize($fn_ctr));
fclose($fh);
} else {
$counter = 0;
}
//If we haven't forced an update for a while, let's force one now by deleting the tracker file.
//Let's also take advantage of this time to trim our log files.
if ($counter &gt;= 5) {
if (file_exists($fn_log2)) {
$result = unlink($fn_log2);
echo "Deleting old log filen";
}
if (file_exists($fn_log)) {
$result = rename($fn_log, $fn_log2);
echo "Renaming the current log filen";
}
if (file_exists($fn_ctr)) {
$result = unlink($fn_ctr);
echo "Deleting the counter filen";
}
if (file_exists($fn_ip_address)) {
$result = unlink($fn_ip_address);
echo "Deleting the tracker filen";
}
$counter = 0;
}
//Let's check to see if our IP address tracker file exists. If it does, let's open the file
//read its contents and then close the file.
if (file_exists($fn_ip_address)) {
$fh = fopen($fn_ip_address, 'r') or die("can't open tracker file");
$ip_previous = fread($fh, filesize($fn_ip_address));
fclose($fh);
echo "Previous: " . $ip_previous . "n";
//Let's compare the address we just scraped off the router page to what we captured last time.
//If the addresses are the same then quit here.
if (trim($ip_previous) == $ip_match) {
echo "Address has not changed .... no action taken" . "nn";
} else {
//Since our address has changed, let's update our tracker file and OPENDNS
echo "Address is different .... updating DNS" . "nn";
$fh = fopen($fn_ip_address, 'w') or die("can't open file");
fwrite($fh, $ip_match);
fclose($fh);
dns_updater($dns_url);
twitter_update($twitterusername,$twitterpassword,'DNS-o-Matic Updated');
}
} else {
//Since our tracker file doesn't exist then we'll create it and force an OPENDNS update
$fh = fopen($fn_ip_address, 'w') or die("can't open file");
fwrite($fh, $ip_match);
fclose($fh);
dns_updater($dns_url);
twitter_update($twitterusername,$twitterpassword,'DNS-o-Matic Updated');
$counter = 0;
}
$counter = $counter + 1;
$fh = fopen($fn_ctr, 'w') or die("can't open counter file for writting");
fwrite($fh, $counter);
fclose($fh);
//Function to get the data from a URL
function get_data($url)
{
$ch = curl_init();
$timeout = 5;
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,$timeout);
$data = curl_exec($ch);
curl_close($ch);
return $data;
}
function dns_updater($url)
{
//Let's prepare to run the update by creating a new cURL resource
$ch = curl_init();
// Let's set our URL and other appropriate options
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER,false);
//Let's fetch the URL and thus perform our update
curl_exec($ch);
//Let's close our cURL resource, and free up system resources
curl_close($ch);
}
function twitter_update($username, $password, $message)
{
$message = 'is twittering from php using curl';
// The twitter API address
$url = 'http://twitter.com/statuses/update.xml';
// Alternative JSON version
// $url = 'http://twitter.com/statuses/update.json';
// Set up and execute the curl process
$curl_handle = curl_init();
curl_setopt($curl_handle, CURLOPT_URL, "$url");
curl_setopt($curl_handle, CURLOPT_CONNECTTIMEOUT, 2);
curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl_handle, CURLOPT_POST, 1);
curl_setopt($curl_handle, CURLOPT_POSTFIELDS, "status=$message");
curl_setopt($curl_handle, CURLOPT_USERPWD, "$twitterusername:$twitterpassword");
$buffer = curl_exec($curl_handle);
curl_close($curl_handle);
// check for success or failure
if (empty($buffer)) {
return 'message';
} else {
return 'success';
}}
?>

The script works by “scraping” the IP address for the external router interface off of my UVerse router status page. That means you’ll need to modify the script to work for you.  Since I’m using a regular expression to find the IP address, all you should have to do is point the script to the correct page on the router.

Don’t forget to modify the script with your credentials and other specifics.

Happy coding!

~GT~