Build your own dynamic DNS with GANDI API

Recently as I wanted to move from Adsl to optical fiber, I had to change my ISP.
Unfortunately, my new ISP does not provide a static IP as the old one did.

So I had to figure out how to keep a few things working with an dynamic ip address:
- I host a few web sites
- I use OpenDNS as parental filter
- On a distant site, I use the source IP address on the firewall for NAT translation

In this article, I will speak about detecting the change of the IP address and how to update it in the domain name registrar: Gandi.

Short version

If you are in a hurry, this piece of code with the following libraries should be good enough.
Adjust the parameters, put in in a cron and you are done.
- Gandi library : https://tools.tribulations.eu/hmoreau/MyDynDNS/blob/master/Gandi.php
- IPUpdater library : https://tools.tribulations.eu/hmoreau/MyDynDNS/blob/master/IPUpdater.php

You will need to get your API KEY from Gandi and you will need to get the id of the Zone you want to edit. To see how to get it, read the "building-a-wrapper" paragraph.

<?php
require('Gandi.php');
require('IPUpdater.php');
 
Gandi::setKey("prod","ApiKeyForProduction");
Gandi::SetEnvironement("prod");
 
/* Check if local ip has changed */
$ipCheck = new IPUpdater();
$ip = $ipCheck->checkChange("localhost");
if(!is_null($ip))
{
	$zoneId = 123456; // Zone id that needs to be updated. 
	GandiDomainZone::updateRecord($zoneId,"recordname","A",$ip,300);
	echo "New external IP : ".$ip."\r\n";
}
else
{
	echo "No change for external ip\r\n";
}
?>

Detect the IP Adress change

Get the external IP Adress

This part is the easiest part.
All we have to do is calling the webservice : http://checkip.dyndns.com/ which will return you current external IP Address.

<?php
	$externalContent = file_get_contents('http://checkip.dyndns.com/');
	preg_match('/Current IP Address: \[?([:.0-9a-fA-F]+)\]?/', $externalContent, $m);
	$externalIp = $m[1];// you now have your externip address like 1.2.3.4
?>

Detect the change

To know if the external IP address you just retrieved has changed, you have to check it against and older value.
It means that every time you ckeck the external address, you have to store it in order to retrieve it later.

I put all the code that allow to get the external IP address, store it in a flat file and detect the change in a single library, the IPUpdater library : https://tools.tribulations.eu/hmoreau/MyDynDNS/blob/master/IPUpdater.php.

<?php
require('IPUpdater.php');
$ipCheck = new IPUpdater();
$ip = $ipCheck->checkChange("localhost");
if(!is_null($ip))
{
	echo "New external IP : ".$ip."\r\n";
}
else
{
	echo "No change for external ip\r\n";
}
?>
 

Gandi API

Gandi has quite an extensive API for manipulating nearly everything on your Gandi account : certificates, hosting, sites, domains ...
We will be digging into one part of this huge API : the domain API, and more specifically into the Domain.Zone part of this API.

Gandi domain model

Before manipulating the API we have to figure out how Gandi handles data: the model.

Everythin starts with the zone files :
"A Domain Name System (DNS) zone file is a text file that describes a DNS zone. A DNS zone is a subset, often a single domain, of the hierarchical domain name structure of the DNS." (source: Wikipedia)

Gandi has a nice way to manage the zone files: the files are versioned.
Other nice feature : you cannot directly edit the live zone file : you have to duplicate the active file, edit it and set it as the new live.

In term of data model it means that we will have to handle multiple version of a zone file and know which one is the live one.

Then each version of the zone file has DNS records.

So we have something like this :

 
Domain
  Zone
    Zone Version
      Record
 

A Zone file has a few properties :
- id: id of the Zone
- domains: Number of domains linked to this zone
- name: Name of the Zone file
- owner: Gandi ID of the owner of the Zone file
- version: id of the current active version
- versions: list of ids of all available version
- date_updated: date of last update

A Zone Version is quite light:
- id: id of the Zone Version
- date_created: date of creation of the version

A record is straightforward:
- id: id of the record
- name: name of the record
- ttl: TTL in seconds
- type: DNS record type (A, AAAA, CNAME, MX ...)
- value: value of the record

Using the API

"Gandi provides remotes APIs using the XML-RPC protocol making it easy to build third party applications to manage your Gandi resources (domains, contacts, hosting, etc)."(Source : Gandi API documentation)

So we will have to use the XML/RPC2 library from PEAR to access the API with php.

Here is an example of how it works.
In this example, we get the list of all zone you have access to.

<?php
 
require_once 'XML/RPC2/Client.php';
require('HTTP/Request2.php');
 
// on my system, I had to specify the use of curl
$request = new HTTP_Request2('https://rpc.ote.gandi.net/xmlrpc/', HTTP_Request2::METHOD_POST);
$request->setAdapter('curl');
 
// Curl has its own CA bundle so I had to disable the sslverify to make it work
$version_api = XML_RPC2_Client::create(
    'https://rpc.gandi.net/xmlrpc/',
    array( 'prefix' => 'domain.zone.', 'sslverify' => false, 'httpRequest'=>$request)
);
 
$apikey = 'my 24-character API key';
 
$result = $version_api->__call('list',array($apikey));
 
print_r($result);
?>

This gave me this result :

 
Array
(
    [0] => Array
        (
            [date_updated] => stdClass Object
                (
                    [scalar] => 20160413T22:34:31
                    [xmlrpc_type] => datetime
                    [timestamp] => 1460586871
                )
            [id] => 123456
            [version] => 14
            [public] => 
            [name] => MyTestZone
        )
)
 

Building a wrapper

Using this API directly like that might be a bit tedious.
As I am a bit lazy, I prefered to build an OOP wrapper.

The wrapper is avaliable here : https://tools.tribulations.eu/hmoreau/MyDynDNS/blob/master/Gandi.php

Now here is what we would write with the wrapper in order to get more or less the same result:

<?php
 
require('Gandi.php');
 
Gandi::setKey("prod","my 24-character API key");
Gandi::SetEnvironement("prod");
// setting this to true will automatically enable recursion and fetch underlying Zone Versions and Records
Gandi::$autoHydrate = false;
 
$zone = new GandiDomainZone();
$result = $zone->getList();
print_r($result);
?>

This, will give the following result, which is more consistent with an OOP approach.
 
Array
(
    [0] => GandiDomainZone Object
        (
            [prefix:protected] => domain.zone.
            [zoneId:protected] => 123456
            [name:protected] => MyTestZone
            [owner:protected] => HM1-GANDI
            [version:protected] => 14
            [versions:protected] => Array
                (
                )
 
        )
 
)
 

If we activate the auto hydration mode

<?php Gandi::$autoHydrate = true; ?>

Whe woud have had something like this:

 
Array
(    
    [0] => GandiDomainZone Object
        (
            [prefix:protected] => domain.zone.
            [zoneId:protected] => 123456
            [name:protected] => MyTestZone
            [owner:protected] => HM1-GANDI
            [version:protected] => 14
            [versions:protected] => Array
                (                    
                    [13] => GandiDomainZoneVersion Object
                        (
                            [prefix:protected] => domain.zone.version.
                            [versionId:protected] => 13
                            [zone:protected] => GandiDomainZone Object *RECURSION*
                            [records:protected] => Array
                                (
                                    [987654] => GandiDomainZoneRecord Object
                                        (
                                            [prefix:protected] => domain.zone.record.
                                            [version:protected] => GandiDomainZoneVersion Object *RECURSION*
                                            [recordId:protected] => 987654
                                            [name:protected] => @
                                            [value:protected] => 1.2.3.4
                                            [ttl:protected] => 10800
                                            [type:protected] => A
                                        )
                                )
                        )
                    [14] => GandiDomainZoneVersion Object
                        (
                            [prefix:protected] => domain.zone.version.
                            [versionId:protected] => 14
                            [zone:protected] => GandiDomainZone Object *RECURSION*
                            [records:protected] => Array
                                (
                                    [456789] => GandiDomainZoneRecord Object
                                        (
                                            [prefix:protected] => domain.zone.record.
                                            [version:protected] => GandiDomainZoneVersion Object *RECURSION*
                                            [recordId:protected] => 456789
                                            [name:protected] => @
                                            [value:protected] => 1.2.3.4
                                            [ttl:protected] => 10800
                                            [type:protected] => A
                                        )                                    
                                    [3456789] => GandiDomainZoneRecord Object
                                        (
                                            [prefix:protected] => domain.zone.record.
                                            [version:protected] => GandiDomainZoneVersion Object *RECURSION*
                                            [recordId:protected] => 3456789
                                            [name:protected] => www
                                            [value:protected] => 1.2.3.4
                                            [ttl:protected] => 10800
                                            [type:protected] => A
                                        )
                                )
                        )
                )
        )
)
 

Here is the footprint of the wrapper

 
class Gandi
	public function __construct()
	public function __call( $name,$arguments )
	public static function setEnvironement($env=null)
	public static function setKey($env,$key)
 
class GandiDomainZone extends Gandi
	public function __construct($zoneId = null)
	public function info()
	public function setZoneId($zoneId)
	public function getVersion($versionId)
	public function getList()
	public function getId()
	public function getName()
	public function getOwner()
	public function getActiveVersion()
	public function getVersions()
	public static function export($zoneId,$full=false)
	public static function import($zoneId,$data,$set=false)
	public static function updateRecord($zoneId,$recordName,$recordType,$newValue,$newTtl=null)
 
class GandiDomainZoneVersion extends Gandi
	public function __construct(&$zone, $versionId = null)
	public function setVersionId($versionId)
	public function delete()
	public function count()
	public function add($fromVersion = null)
	public function set()
	public function getZone()
	public function getId()
	public function getRecords()
 
class GandiDomainZoneRecord extends Gandi
	public function __construct(&$version,$recordId = null, $name = null, $value = null, $ttl = null, $type = null)
	public function getList()
	public function add($name,$value,$ttl,$type)
	public function delete()
	public function update()
	public function findByNameAndType($name,$type)
	public function getId()
	public function getName()
	public function getValue()
	public function getTtl()
	public function getType()
	public function setName($name)
	public function setValue($value)
	public function setTtl($ttl)
	public function setType($type)
 

Using the wrapper to update a record

Now we want to use the API Wrapper to update a record

 
<?php
require('Gandi.php');
Gandi::setKey("prod","my 24-character API key");
Gandi::SetEnvironement("prod");
// 123456 is the id of the Zone we want to update
GandiDomainZone::updateRecord(123456,"www","A",'4.3.2.1',300);
?>

Final thoughts

We can now use the script from the "Short version" paragraph and put it in a file that will be called by a cron job in order to perform the tests at least once an hour.

<?php
require('Gandi.php');
require('IPUpdater.php');
 
Gandi::setKey("prod","ApiKeyForProduction");
Gandi::SetEnvironement("prod");
 
/* Check if local ip has changed */
$ipCheck = new IPUpdater();
$ip = $ipCheck->checkChange("localhost");
if(!is_null($ip))
{
	$zoneId = 123456; // Zone id that needs to be updated. 
	GandiDomainZone::updateRecord($zoneId,"recordname","A",$ip,300);
	echo "New external IP : ".$ip."\r\n";
}
else
{
	echo "No change for external ip\r\n";
}
?>

The Gandi API wrapper and the IPUpdater class can perform much more than that.
I will let you dig in to the code to see for yourself.

postheadericon Commentaires