Dynamic update IP with PPP connections

I made a perl script (Steffen Beyer, cpunk@reactor.de provided improvements for version 1.2.) that can be used on BSD, Mac OS X (and probably Linux too) to update the dynamic IP of a domain or machine with the name servers of EasyDNS.com and DynDNS.org. If you bought a domain from EasyDNS.com or use the free DynDNS.org, keep reading.

For historical reasons, the script (or dynamic IP update client) is called easydnsupdate because that is how it started: as an easydns client. Now it can also update your IP at DynDNS and will support others eventually. This simple Perl script sends a HTTP (or secure HTTPS) request to the name server with the username, password and domain information. On Unix systems with ppp connections, when the connection is up the script /etc/ppp/ip-up is called. I modified ip-up so that it calls /usr/local/bin/easydnsudpate with the network interface of the main connection and its current IP address. A configuration file /usr/local/etc/easydnsudpate.conf is used to keep the information about your domain, but you can override that (see below).

Requirements

All the required files are available here: easydns.tar. The instructions below assumes a Mac OS X system, but were also tested on OpenBSD. Untar the files with tar xvf easydns.tar, then read INSTALL, which you can execute with sudo sh INSTALL. You then need to edit the file /usr/local/etc/easydnsupdate.conf with your personal information.

Open and terminal and type:


cd
curl -O http://www.novajo.ca/easydns.tar
tar xvf easydns.tar
cd easydns
sudo sh INSTALL
sudo pico /usr/local/etc/easydnsupdate.conf

The last line (sudo pico /usr/local/etc/easydnsupdate.conf) open a pico session to edit the configuration line. The configuration file looks like this:

easydnsupdate.conf:


# This is the configuration file for easydnsupdate. 
#
# There are several dynamic IP services available on the web
# The following are currently supported:
#
# www.EasyDNS.com (any domain).  Use service name "easydns"
# DynDNS.org (any domain).  They provide three different services
#	* regular DynDNS service: use service name "dyndns"
#	* static DNS service: use "statdyndns"
#	* custom DNS service: use "customdyndns"
# other can be added to the main perl script.
#
# The fields are separated by white spaces or tabs.  Anything after the domain is optional,
# but if you want to use the other options afterwards, you must explicitly set the
# parameters in between.
#
# service is one of the supported services (see above, ex: easydns)
# username is your username on their system (novajo)
# password is your password on their system (ex: passowrd)
# domain is the domain name you want to update (ex: novajo.ca)
# mx must be an ip address that points to a mailexchanger (ex: 128.100.75.10). Set to no,false,0 or * if not using.
# backmx is whether or not the mailexchanger is used (ex: no)
# wilcard will map anything suffixed with your domain name to the machine your domain resolves (ex: yes)
# secure determines if you use SSL to connect or not (ex: no).
#
# To use secure connections, the service must support them AND you must have Crypt::SSLeay
# on your system.
#
# backmx, wildcard and secure must be either (1,on,true,yes) or (0,off,false,no)
#
# Because passwwords are stored cleartext, you might want to
# make this file readable only by the root user (or actually
# the user that will run the easydnsupdate script).  If you
# use pppd and ip-up, this is the user pppd runs as (root)
#
# service	username	password	domain	mx	backmx	wildcard	secure

Enter the service, your username, password, domain, mail exchange IP and whether or not you want to use backup mail servers, wildcards and SSL to connect. Save your work. To test that everything is fine, type:

Test configuration with the system configuration file:


sudo easydnsupdate  ppp0
grep easydnsupdate /var/log/system.log

Test configuration with the configuration file in current directory:


sudo easydnsupdate -c easydnsudpate.conf ppp0
grep easydnsupdate /var/log/system.log

Force an update with the -f option:


sudo easydnsupdate -f -c easydnsudpate.conf ppp0
grep easydnsupdate /var/log/system.log

You should see something like:

May 19 23:40:09 localhost easydnsupdate[7956]: Domain: thedomain.com at easydns updated to point to 64.229.96.166 
May 19 23:40:10 localhost easydnsupdate[7956]: Domain: thedomain.homeip.net at dyndns updated to point to 64.229.96.166 

If there is a problem, check out the file /tmp/easydnsupdate.log. Then disconnect, and reconnect (using Internet Connect for instance). Your IP will be automatically updated if it is needed.

The "whole program" is also available via anonymous CVS if you prefer with the following CVSROOT variable:

setenv CVSROOT :pserver:anonymous@cvs.novajo.ca:/usr/local/CVS

with no password. The name of the project is easydns. Therefore you check it out with cvs checkout easydns. Fell free to improve the source and send me your changes as a patch (diff -ur old-tree/ new-tree/).

For reference, this is the main script:

easydnsupdate:


#!/usr/bin/perl -w

#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

# Script to update a dynamic IP with easydns service on any Unix based system with Perl.
# (BSD, Mac OS X, Linux and others).  Tested on OS X, OpenBSD 3.1 and GNU/Linux 2.4.
# You need to configure the username, password, domain in /usr/local/etc/easydnsupdate.conf 
# to make it work for you. Make sure, the file is not world-readable!
#
# You might need to install some Perl modules.  The best way to do it is to use the CPAN
# module:
# perl -MCPAN -e shell
#
# and type install Bundle::libnet (for LWP) and install Crypt::SSLeay (for SSL support)
# if you want to use secure connections.
#
# Mac OS X users:  be careful when installing LWP (libnet) because it installs a HEAD program
# that could overwrite the head program in /usr/bin/.  Make sure your configuration
# is such that HEAD will get installed in /usr/local/bin/. The HFS file system 
# is not case sensitive. If you overwrite head by mistake, take a fresh one from the install CD.
#
# OpenBSD users: you need to install p5-Crypt-SSLeay and p5-libwww, both found as prepared
# packages.
#
# Typically, you would copy this script to /usr/local/bin/easydnsupdate
# and add these lines to /etc/ppp/ip-up :
#
# -- cut here 
# # Parameters are passed to ip-up (see man pppd).
# # We get the ip address
# ip=$4
#
# logger -i -t $0 "Updating IP with EasyDNS.com"
# /usr/local/bin/easydnsupdate $ip
# -- cut here
#
# If ip-up does not exist (it is a file that gets called automatically when your connection
# comes up) then create it.  It must be executable and start with #!/bin/sh
#
# There is "some" protection to avoid updating the address if it has not changed 
# since last time it was updated. It is safe to call easydnsupdate from the command-line
# as long you you pass the interface and ip address.  If you don't pass the IP address,
# it will try to get it.
#
# If you have problems, everything is logged into /var/log/system.log (may differ, depending on
# your syslog configuration)
#
# Contact:  Daniel Cote mailto:dccote@novajo.ca
# http://www.novajo.ca/easydns.html
# Feel free to use and abuse. Just leave my name somewhere in the header.
#
# Modifications for 1.2 done by Steffen Beyer, cpunk@reactor.de

# Principle of operation
#
# 1) Process options and arguments
# 2) Read configuration file and keep an array of hash references
#    of all domains with their relevant information 
# 3) Check if update required
# 4) Go through all domains form configuration file
#    4.1) Build URL for each specific service
#	 4.2) "Call" URL
#    4.3) Check result based on service
#	 4.4) Log meaningful message through system facilities

# History:
# Version	Date		Changes
# -------------------------------------------------------------------------------------------
# 1.0		05/20/2002	initial release
# 1.1		06/22/2002	user-agent is beeing transmitted as required by the DynDNS
#				specification. / if the IP is specified on the command line,
#				the interface can be left out (wasn't used anyway). / bugfix:
#				no update is tried if an unknown interface is specified. /
#				added simple HTTP error handling. / minor changes.
#				(Steffen Beyer, cpunk@reactor.de)
# 1.2		06/24/2002	bugfix: script died without libwww installed. / ifconfig
#				based IP detection now works under GNU/Linux. / bugfix:
#				negative response from server did not prevent ipaddr file
#				from beeing updated. / minor changes.
#				(Steffen Beyer, cpunk@reactor.de)
# 1.3		08/04/2003 the preferred fetching method is saved
#               in a file ~/easydnsudpate

use strict;
use Sys::Syslog qw(:DEFAULT setlogsock);
use vars qw ($opt_f $opt_c);
use Getopt::Std;


# Global variable that could be changed
# this file (which is not critical)
# keeps the IP address when the last
# update was performed to avoid abuse
my $ipfile = "/tmp/easydns_ipaddr";
# set to one of the following to avoid a directory scan on each run:
# "LWP"		- use perl's libwww interface
# "curl"	- use 'curl' software found in path
# "wget"	- use 'wget' software found in path
my $fetchURLmethod = "";
my $myname = "easydnsupdate";
my $version = "1.3";

#
# Read parameters passed via command-line
#
my $ip;
my $configfile = "/usr/local/etc/easydnsupdate.conf";
my $forceupdate;

my $usage = "Usage:  $myname [-f] [-c configfile] interface|ip_address\n"
	."-f forces the update no matter what\n"
	."-c provides a configuration file other than $configfile\n"
	."interface is the network interface (ppp0, en0, tun0, etc...)\n"
	."ip_address is the ip address you want to use for the update.  If not provided, the script will try to guess using 'ifconfig interface'\n";

# Perl standard function to process options passed through command-line
# They are removed from @ARGV
if (! getopts('fc:')) {
	die "$usage";
} 

if ($opt_c) {
	# Overrides configuration file
	$configfile = $opt_c;
}
if ($opt_f) {
	# Force update even if IP has not changed
	$forceupdate = 1;
}

if (scalar @ARGV == 1) {
	my $reg_decbyte = '([01]?\d{1,2}|2[0-4]\d|25[0-5])';
	if ($ARGV[0] =~ /^$reg_decbyte\.$reg_decbyte\.$reg_decbyte\.$reg_decbyte$/) {
		# argument is a v4 IP (thanks to Alan Derk for the regex)
		$ip = $ARGV[0];
	} else {
		# else assume interface device
		$ip = `/sbin/ifconfig $ARGV[0] 2> /dev/null | grep inet | head -1 | awk '{ print \$2 }'` || die "ifconfig returned error.\n";
		# GNU/Linux specific
		if ($ip =~ /:/) { $ip = $' };
		chomp($ip);
	}
} elsif (scalar @ARGV == 2) {
	# old argument format
	$ip = $ARGV[1];
} else {
	die "$usage";
}

if (-e "$ENV{HOME}/.easydnsudpate") {
	$fetchURLmethod = `cat $ENV{HOME}/.easydnsudpate`;
}

# Read configuration file

my @domainlist;

unless (open CONFIGFILE, $configfile) {
	die "Cannot open config file.  Make sure you have read permission on the file $configfile.\n";
}

while () {
	if (m|^#|) {
		next;
	} elsif (m|(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S*)\s+(\S*)\s+(\S*)\s+(\S*)|i) {
		my $entryref = {"service" => $1,
						"username" => $2,
						"password" => $3,
						"domain"   => $4,
						"mx"       => $5,
						"backmx"   => $6,
						"wildcard" => $7,
						"secure"   => $8,
						 };

		if ( ! ($entryref->{mx} =~ m|\S+\.\S+|i) ) {
			$entryref->{mx} = 0;
		} 
				
		if ($entryref->{backmx} =~ /y(es)?|t(rue)?|on|1/i) {
			$entryref->{backmx} = 1;
		} else {
			$entryref->{backmx} = 0;
		}

		if ($entryref->{wildcard} =~ /y(es)?|t(rue)?|on|1/i) {
			$entryref->{wildcard} = 1;
		} else {
			$entryref->{wildcard} = 0;
		}

		if ($entryref->{secure} =~ /y(es)?|t(rue)?|on|1/i) {
			$entryref->{secure} = 1;
		} else {
			$entryref->{secure} = 0;
		}

		push @domainlist, $entryref;

	}
}
close CONFIGFILE;


# Check if IP address has changed
if (! $forceupdate) {
	my $ipatlastupdate = "";
	if (-e "$ipfile") {
		$ipatlastupdate = `cat $ipfile`;
		chomp($ipatlastupdate);
	} else {
		`touch $ipfile;chmod oug+r $ipfile`;
	}

	if ($ip eq $ipatlastupdate) {
		Logger("No update needed since IP has not changed.");
		exit;
	}
}

# Update domains
my $entryref;
foreach $entryref (@domainlist) {
	my $url = URLForService($entryref);
	
	if ($url ne "invalid service") {
		my $response = FetchURL($url);	

		AnalyzeResult($response, $entryref);
		`echo -n "$ip" > "$ipfile"`;
	}
		
}

# We are done. Below are subroutines used for clarity.



sub URLForService
{
	my ($domainref) = @_;
	 
	my $service  = $domainref->{service};
	my $username = $domainref->{username};
	my $password = $domainref->{password};
	my $domain   = $domainref->{domain};
	my $secure   = $domainref->{secure};

	my $url;

	# If you want to add a new service, do that here.  Email me your changes at dccote@novajo.ca
	# Also modify AnalyzeResult() for returned text;
	
	if ($service =~m|easydns|i) {
		$url  = "$username:$password\@members.easydns.com/dyn/dyndns.php?hostname=$domain&myip=$ip";
		$url .= "&mx=".$domainref->{mx} if ($domainref->{mx} != 0);
		$url .= "&backmx=YES" if ($domainref->{backmx} != 0);
		$url .= "&wildcard=ON" if ($domainref->{wildcard} != 0);		
	} elsif ($service =~m|customdyndns|i) {
		$url  = "$username:$password\@members.dyndns.org/nic/update?system=custom&hostname=$domain&myip=$ip";
		$url .= "&mx=".$domainref->{mx} if ($domainref->{mx} != 0);
		$url .= "&backmx=YES" if ($domainref->{backmx} != 0);
		$url .= "&wildcard=ON" if ($domainref->{wildcard} != 0);		
	} elsif ($service =~m|statdyndns|i) {
		$url  = "$username:$password\@members.dyndns.org/nic/update?system=statdns&hostname=$domain&myip=$ip";
		$url .= "&mx=".$domainref->{mx} if ($domainref->{mx} != 0);
		$url .= "&backmx=YES" if ($domainref->{backmx} != 0);
		$url .= "&wildcard=ON" if ($domainref->{wildcard} != 0);		
	} elsif ($service =~m|dyndns|i) {
		$url  = "$username:$password\@members.dyndns.org/nic/update?system=dyndns&hostname=$domain&myip=$ip";
		$url .= "&mx=".$domainref->{mx} if ($domainref->{mx} != 0);
		$url .= "&backmx=YES" if ($domainref->{backmx} != 0);
		$url .= "&wildcard=ON" if ($domainref->{wildcard} != 0);		
	} else {
		$url = "invalid service";
		Logger("Invalid service: $service.  Check config file $configfile.","err");
	} 
	
	if ($secure) {
		$url = "https://".$url;
	} else {
		$url = "http://".$url;
	}

	return $url;
}

sub AnalyzeResult {
	my ($text, $entryref) = @_;
	
	my $service = $entryref->{service};
	my $message = "Domain: ".$entryref->{domain}." at $service";
	my $success = 0;

	# If you want to add a new service, add error management here.  Email me your changes at dccote@novajo.ca
	# Also modify URLForService() to build url;
	if ($service =~m|easydns|i) {
		if ($text =~ m|NOERROR|i) {
			$message .= " updated to point to $ip";
			$success = 1;
		} elsif ($text =~ m|NOACCESS|i) {
			$message .= " authentication failed.  Check username and password.";
		} elsif ($text =~ m|ILLEGAL INPUT|i) {
			$message .= " error in client.  Contact dccote\@novajo.ca.";
		} elsif ($text =~ m|NOSERVICE|i) {
			$message .= " dynamic DNS is not turned on for this domain.  Go to main easydns web site to activate.";
		} elsif ($text =~ m|TOOSOON|i) {
			$message .= " cannot be updated right now (not enough time since last update).";
		} else {
			$message .= " generated this unknown error: $text.  Please contact dccote\@novajo.ca.";
		}
	} elsif ($service =~m|dyndns|i) {
		if ($text =~ m|badauth|i) {
			$message .= " authentication failed.  Check username and password.";
		} elsif ($text =~ m|badsys|i) {
			$message .= " error in client.  Contact dccote\@novajo.ca.";
		} elsif ($text =~ m|badagent|i) {
			$message .= " updating agent has been blocked. Contact dccote\@novajo.ca.";
		} elsif ($text =~ m|good|i) {
			$message .= " updated to point to $ip";
			$success = 1;
		} elsif ($text =~ m|nochg|i) {
			$message .= " unnecessarily updated to point to $ip. Avoid using the -f option.";
			$success = 1;
		} elsif ($text =~ m|notfqdn|i) {
			$message .= ". A fully qualified domain was not provided.";
		} elsif ($text =~ m|nohost|i) {
			$message .= " does not refer to an existing hostname";
		} elsif ($text =~ m|!donator|i) {
			$message .= " cannot be set offline since your are not a donator";
		} elsif ($text =~ m|!yours|i) {
			$message .= " is not yours and was not updated.";
		} elsif ($text =~ m|!active|i) {
			$message .= " is not active.  Go to main dydns.org web site to activate.";
		} elsif ($text =~ m|abuse|i) {
			$message .= " has been blocked for abuse.  Contact DynDNS.org to be unblocked.";
		} elsif ($text =~ m|numhost|i) {
			$message .= " has generated an error at dydns. You should contact support\@dyndns.org.";
		} elsif ($text =~ m|dnserr|i) {
			$message .= " has generated an error at dydns. You should contact support\@dyndns.org.";
		} elsif ($text =~ m|w(\d+)h|i) {
			$message .= " cannot be updated for another $1 hour(s) and will not be updated now.";
		} elsif ($text =~ m|w(\d+)m|i) {
			$message .= " cannot be updated for another $1 minute(s) and will not be updated now.";
		} elsif ($text =~ m|w(\d+)s|i) {
			my $sleeptime = $1;
			sleep($sleeptime);
			$message .= " had to wait $sleeptime second(s) before being updated to point to $ip";
		} elsif ($text =~ /911|999/i) {
			$message .= ". Things are not working well at dyndns.org right now: $text";
		} else {
			$message .= " generated this unknown error: $text.  Please contact dccote\@novajo.ca.";
		}

	}

	Logger($message);
	exit 1 if (!$success);

}

sub FetchURL {
	my ($url) = @_;
	
	# The first time, we will have to determine how to fetch the URL
	if (! $fetchURLmethod) {
		my @PATH = split /:/, $ENV{PATH};

		my $hasLWP  = `find @INC -name 'LWP.pm' -print 2>/dev/null`;
		my $hasWget = `find @PATH -name 'wget' -print 2>/dev/null`;
		my $hasCurl = `find @PATH -name 'curl' -print 2>/dev/null`;

		if ($hasLWP) {
			$fetchURLmethod = "LWP";
		} elsif ($hasCurl) {
			$fetchURLmethod = "curl";
		} elsif ($hasWget) {
			$fetchURLmethod = "wget";
		} else {
			die "Unable to fetch URL: this system does not have one of the following: LWP, curl or wget.  You must install one.\n";
		}
		`echo -n $fetchURLmethod >> $ENV{HOME}/.easydnsudpate`;
	}
	
	my $returnedtext;
	my $failure = "HTTP request to server failed.";
	my $fetchURLcommand;
	
	if ($fetchURLmethod =~ /curl/i) { $fetchURLcommand = "curl -s -A $myname/$version-curl"; }
	if ($fetchURLmethod =~ /wget/i) { $fetchURLcommand = "wget -qnv -O- -U $myname/$version-wget"; }
	
	if ($fetchURLmethod =~ /LWP/i) {
		require LWP;

		my $ua = LWP::UserAgent->new(agent => "$myname/$version-LWP");
		my $req = HTTP::Request->new(GET => "$url");
		my $res = $ua->request($req);
		if ($res->is_error()) {
			Logger($failure,"err");
			exit 1;
		}
 		$returnedtext = $res->content();
	} else {
		$returnedtext = `$fetchURLcommand "$url"`; 
		if ($?) {
			Logger($failure,"err");
			exit 1;
		}
	}
	
	return $returnedtext;

}

sub Logger {
        my ($message, $level) = @_;
 
        $level = 'notice' unless $level;

        setlogsock('unix');

        openlog("$myname",'','user');
        syslog($level,$message);
        closelog();
}

The file /etc/ppp/ip-up gets modified to look like this:

Typical ip-up:


#!/bin/sh

# Begin DynamicDomainPPP

# Parameters are passed to ip-up (see man pppd).
# We get ip address 
ip=$4

logger -i -t $0 "Updating IP with Dynamic IP Domain Name Servers"
/usr/local/bin/easydnsupdate $ip

# End DynamicDomainPPP

Contact information

You can email me at dccote@novajo.ca for corrections, comments or questions.