Dynamic DNS support for WHM using ddclient
The server which I use to use to host my websites is now primarily used as my DNS and also a backup ftp space for this server to store backups (that way I don't have to buy some sort of tape backup thing). This is great except for the fact that my IP changes a couple of times a year. Up until now I was pushing requests from the webhost's DNS to dyndns and then to my home server running ddclient to update dyndns. Of course I could have paid around $30 a year per domain but I'm cheap.
So I modified ddclient so that it would update my host's DNS server. My host uses cpanel/WHM which luckily has a JSON API!
Here's how you can make the same modifications:
Install ddclient on your server - this is easy
yum install ddclient
You're also going to need some perl mods:
cpan
note: don't just copy and paste these 4 lines at once, some of the installs may have you type "yes" at some point. In the case of WWW::Mechanize, it forced me to type "yes" at least 4 times.
install JSON install JSON::XS install WWW::Mechanize exit
Now, make sure you have version 3.7.3 of ddclient and then patch it with this ddclient 3.7.3 patch file
cd /tmp wget http://blog.bennyland.com/wp-content/uploads/2010/01/ddclient.tar.gz tar -zxvf ddclient.tar.gz cd ddclient patch -p0 /usr/sbin/ddclient < ddclient.patch
Alternatively you can follow these directions:
vim /usr/sbin/ddclient
require 5.004; use strict; use Getopt::Long; use Sys::Hostname; use IO::Socket;
with:
require 5.004; use strict; use Getopt::Long; use Sys::Hostname; use IO::Socket; use WWW::Mechanize; use JSON -support_by_pp;
'sitelutions' => {
'updateable' => undef,
'update' => \&nic_sitelutions_update,
'examples' => \&nic_sitelutions_examples,
'variables' => merge(
{ 'server' => setv(T_FQDNP, 1, 0, 1, 'www.sitelutions.com', undef) },
{ 'min-interval' => setv(T_DELAY, 0, 0, 1, 0, interval('5m')),},
$variables{'service-common-defaults'},
),
},
with:
'sitelutions' => {
'updateable' => undef,
'update' => \&nic_sitelutions_update,
'examples' => \&nic_sitelutions_examples,
'variables' => merge(
{ 'server' => setv(T_FQDNP, 1, 0, 1, 'www.sitelutions.com', undef) },
{ 'min-interval' => setv(T_DELAY, 0, 0, 1, 0, interval('5m')),},
$variables{'service-common-defaults'},
),
},
'whm' => {
'updateable' => undef,
'update' => \&nic_whm_update,
'examples' => \&nic_whm_examples,
'variables' => merge(
{ 'min-interval' => setv(T_DELAY, 0, 0, 1, interval('5m'), 0),},
$variables{'service-common-defaults'},
),
},
sub split_by_comma {
my $string = shift;
return split /\s*[, ]\s*/, $string if defined $string;
return ();
}
with:
sub split_by_comma {
my $string = shift;
return split /\s*[, ]\s*/, $string if defined $string;
return ();
}
sub split_by_semi {
my $string = shift;
return split /\s*[; ]\s*/, $string if defined $string;
return ();
}
###################################################################### # vim: ai ts=4 sw=4 tw=78 :
with:
######################################################################
######################################################################
## nic_whm_examples
######################################################################
sub nic_whm_examples {
return <<EoEXAMPLE;
o 'whm'
The 'whm' protocol is an API provided by the cpanel/WHM system.
Configuration variables applicable to the 'whm' protocol are:
protocol=whm ##
server=ip.of.your.whm.login ## defaults to sitelutions.com
login=service-login ## login name and password registered with the service
password=service-password ## whm hash used for API (not your whm password)
host.to.update;host.to.update. #
Example ${program}.conf file entries:
## single host update
protocol=whm, \\
login=my-whm-login, \\
password=a-really-long-hash-string \\
myhost.com;myhost.com.;subdomain1.myhost.com.;subdomain2.myhost.com.,myotherhost.com;subdomain2.myotherhost.com.
EoEXAMPLE
}
######################################################################
## nic_whm_update
##
## written by Benny Raymond
##
######################################################################
sub nic_whm_update {
debug("\nnic_whm_update -------------------");
my $has_failed = 0;
## update each configured host
foreach my $h (@_) {
my @host_and_names = split_by_semi($h);
my $host_to_update = $host_and_names[0];
my %hosts_names;
undef %hosts_names;
my $i = 0;
for (@host_and_names) {
if ($i==0) {
$i++;
} else {
$hosts_names{$_} = 1;
}
}
info("setting IP address to %s for %s", $ip, $host_to_update);
verbose("UPDATE:","updating %s", $host_to_update);
my $url_lookup;
$url_lookup = "http://$config{$h}{'server'}/json-api/dumpzone";
$url_lookup .= "?domain=$host_to_update";
my $api_output = json_whm_fetch($config{$h}{'login'},$config{$h}{'password'},$url_lookup);
if (!defined($api_output) || !$api_output) {
failed("updating %s: Could not connect to %s.", $h, $url_lookup);
last;
}
if (json_whm_status($api_output) != 1)
{
failed("updating %s: %s - %s", $h, json_whm_status_msg($api_output), $url_lookup);
}
# loop through names and update
# get the A records
my(@a_records) = @{(json_whm_a_records($api_output))};
# and update the DNS table
foreach (@a_records)
{
my(%record) = %{($_)};
if ($hosts_names{$record{name}})
{
if ($record{address} eq $ip)
{
success("updating '%s' - '%s': good: IP already set to %s", $host_to_update, $record{name}, $ip);
}
else
{
# update this line
# print $record{Line} . ">" . $record{name};
$url_lookup = "http://$config{$h}{'server'}/json-api/editzonerecord";
$url_lookup .= "?domain=$host_to_update";
$url_lookup .= "&Line=$record{Line}";
$url_lookup .= "&address=$ip";
my $api_update_output = json_whm_fetch($config{$h}{'login'},$config{$h}{'password'},$url_lookup);
if (json_whm_status($api_update_output) != 1)
{
failed("updating '%s' - '%s': %s %s", $host_to_update, $record{name}, json_whm_status_msg($api_update_output), $url_lookup);
$has_failed = 1;
$config{$h}{'status'} = 'failed';
} else {
success("updating '%s' - '%s': good: IP address set to %s", $host_to_update, $record{name}, $ip);
}
}
} else {
# warning("name '%s' not found for host '%s'", $record{name}, $host_to_update);
}
}
if ($has_failed == 0)
{
$config{$h}{'ip'} = $ip;
$config{$h}{'mtime'} = $now;
$config{$h}{'status'} = 'good';
}
}
}
sub json_whm_a_records
{
my(%json) = %{(shift)};
my(@records) = @{($json{result}[0]{record})};
my(@a_records);
foreach (@records)
{
my(%record) = %{($_)};
if ($record{type} eq "A")
{
push(@a_records, \%record);
}
}
return \@a_records;
}
sub json_whm_status
{
my(%json) = %{(shift)};
my $status = $json{result}[0]{status};
return $status;
}
sub json_whm_status_msg
{
my(%json) = %{(shift)};
my $status = $json{result}[0]{statusmsg};
return $status;
}
sub json_whm_fetch
{
my ($whm_api_login) = $_[0];
my ($whm_api_key) = $_[1];
my ($json_url) = $_[2];
my $browser = WWW::Mechanize->new();
my @args = (
Authorization => "WHM " . $whm_api_login . ":" . $whm_api_key
);
my $json_text = "";
eval{
# download the json page:
# print "Getting json $json_url\n";
$browser->get( $json_url, @args );
my $content = $browser->content();
my $json = new JSON;
# these are some nice json options to relax restrictions a bit:
$json_text = $json->allow_nonref->utf8->relaxed->escape_slash->loose->allow_singlequote->allow_barekey->decode($content);
};
# catch crashes:
if($@){
print STDERR "[[JSON ERROR]] JSON parser crashed! $@\n";
return "";
} else {
return $json_text;
}
}
######################################################################
# vim: ai ts=4 sw=4 tw=78 :
Save and close ddclient
:wq
Now, edit ddclient.conf.
vim /etc/ddclient/ddclient.conf
Here's a simple version (note: you need to log into WHM and get your API key or none of this is going to work. You'll be entering your API key as one line - in WHM it is copied as several lines, just remove the carriage returns and enter it in one huge line as your password). Notice how my domain looks, it's my domain name, followed by a colon, followed by colon seperated hostnames to update - they all have periods after them!
daemon=300 # check every 300 seconds
syslog=yes # log update msgs to syslog
mail=root # mail all msgs to root
mail-failure=root # mail failed update msgs to root
pid=/var/run/ddclient.pid # record PID in file.
ssl=yes # use ssl-support. Works with
# ssl-library
## To obtain an IP address from Web status page (using the proxy if defined)
use=web, web=checkip.dyndns.org/, web-skip='IP Address' # found after IP Address
##
## WHM
##
server=host.or.ip.of.your.whm.maybe.with.port, \
protocol=whm, \
login=your-whm-username, \
password=crazy-long-api-key-as-one-long-line \
hostname.com;hostname.com.;subdomain.hostname.com.
Save and close ddclient.conf
:wq
And finally run it and tell it to run every time the server reboots:
/sbin/chkconfig ddclient on /sbin/service ddclient start