Friday, 15 February 2013

Dynamic DNS on a Raspberry Pi using Python

I thought I would share my experience of making a simple DDNS client in Python on the Raspberry Pi...

A lot of modern routers have facilities for DDNS, but the DDNS providers they support are not always free. Also with some providers it can take some time for an update to proliferate through the DNS. A good, free service is available at http://freedns.afraid.org/. By good I mean that updates seem to be applied fairly quickly.
Obviously if you're hosting something critical at a remote location on your Raspi then you should probably consider a paid service. I've never had an issue with this particular free DNS service, but as I only live about half an hour's drive away from this particular Raspi it wouldn't be the end of the world if I couldn't access it remotely.

Helpfully the features of this DDNS service include a web page that when requested will return the detected IP address of whatever requested it, and a web page that when requested will update your DDNS record with the detected IP.

This makes it quite easy to write a simple update client in Python. All we need to do is perform a DNS lookup on our DDNS host; and retrieve the web page that detects our IP address. If the answers match then all is well, if not then the update URL can be requested.

Python has a socket module that provides low-level networking capabilities. If you get bored you can read all about it here. We can use something from this module to perform a DNS lookup.

pi@raspberrypi ~/python $ python -i
Python 2.7.3rc2 (default, May  6 2012, 20:02:25)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from socket import getaddrinfo
>>> moo = getaddrinfo('www.google.com',None)
>>> moo
[(2, 1, 6, '', ('173.194.67.104', 0)), (2, 2, 17, '', ('173.194.67.104', 0)), (2, 3, 0, '', ('173.194.67.104', 0)), (2, 1, 6, '', ('173.194.67.147', 0)), (2, 2, 17, '', ('173.194.67.147', 0)), (2, 3, 0, '', ('173.194.67.147', 0)), (2, 1, 6, '', ('173.194.67.106', 0)), (2, 2, 17, '', ('173.194.67.106', 0)), (2, 3, 0, '', ('173.194.67.106', 0)), (2, 1, 6, '', ('173.194.67.103', 0)), (2, 2, 17, '', ('173.194.67.103', 0)), (2, 3, 0, '', ('173.194.67.103', 0)), (2, 1, 6, '', ('173.194.67.99', 0)), (2, 2, 17, '', ('173.194.67.99', 0)), (2, 3, 0, '', ('173.194.67.99', 0)), (2, 1, 6, '', ('173.194.67.105', 0)), (2, 2, 17, '', ('173.194.67.105', 0)), (2, 3, 0, '', ('173.194.67.105', 0)), (10, 1, 6, '', ('2a00:1450:400c:c05::68', 0, 0, 0)), (10, 2, 17, '', ('2a00:1450:400c:c05::68', 0, 0, 0)), (10, 3, 0, '', ('2a00:1450:400c:c05::68', 0, 0, 0))]
>>>

getaddrinfo() returns a list of tuples. Within each tuple there is a tuple that holds the socketaddress for the hostname we specified. The socketaddress is in the form (ip, port). Since we passed the argument None when calling getaddrinfo() the port is returned as 0.
If you call getaddrinfo against the DDNS host that you registered with Free DNS then you won't have quite so many entries in the returned list, but you get the idea.

To get the ip address we want the string in position 0 of the tuple that is in position 4 of the tuple that is in position 0 of the list. Confused? Good, so am I. Perhaps the following will explain it a bit better.


>>> moo[0] # item in position 0 of the list moo
(2, 1, 6, '', ('173.194.67.104', 0)) # a tuple is returned
>>> moo[0][4] # item in position 4 of the tuple in position 0 of the list
('173.194.67.104', 0) # a tuple is returned
>>> moo[0][4][0] # item in position 0 of the tuple in position 4 of the tuple in position 0 of the list
'173.194.67.104' # a string is returned
>>>

Now we need to ask the Free DNS website what it thinks our ip address is. To do this we need to retrieve this webpage and do something with what's returned. To do that we can use the urllib module.


pi@raspberrypi ~/python $ python -i
Python 2.7.3rc2 (default, May  6 2012, 20:02:25)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from urllib import urlopen
>>> urlobj = urlopen('http://freedns.afraid.org/dynamic/check.php')
>>> moo = urlobj.read()
>>> moo
'Detected IP : 81.132.56.189\nHTTP_CLIENT_IP : \nHTTP_X_FORWARDED_FOR : \nREMOTE_ADDR : 81.132.56.189\n'
>>>

urlopen() returns a url object which you can then read from. For what we're doing you don't particularly need to return the url object; you can put the read() bit straight on the end of the variable assignment.


>>> moo = urlopen('http://freedns.afraid.org/dynamic/check.php').read()
>>> moo
'Detected IP : 81.132.56.189\nHTTP_CLIENT_IP : \nHTTP_X_FORWARDED_FOR : \nREMOTE_ADDR : 81.132.56.189\n'
>>>

It would be really handy if we could have this returned as a list, and we can! The read() returns a string object. Since moo is now a string object we can use string functions. We want to split the string, and we don't need to pass any arguments. This too can be appended to the end of the assignment for moo. So now we get a list of strings:

>>> moo = urlopen('http://freedns.afraid.org/dynamic/check.php').read().split()
>>> moo
['Detected', 'IP', ':', '81.132.56.189', 'HTTP_CLIENT_IP', ':', 'HTTP_X_FORWARDED_FOR', ':', 'REMOTE_ADDR', ':', '81.132.56.189']

We want the string in position 3 of the list

>>> moo[3]
'81.132.56.189'

Now we have two strings that we can compare.... e.g.


if not (ddnsip == actualip):
    update_ip()

To update the record held by Free DNS is easy, we can use urlopen again and point it at the URL given in your Free DNS account for updating by the "Direct method".


>>> dummy = urlopen('https://freedns.afraid.org/dynamic/update.php?sdfljjs98234sekjslkdkf=').read()
>>>
Requesting the update URL does return a message saying whether or not it was successful so if you were that bothered you could check for confirmation...but I'm not :)

The script I have written is available here. I have fleshed it out a bit to try and catch errors, and it also writes stuff to the syslog so you can check its operation. To view the most recent bit of the syslog you can simply type the following into a terminal:


tail /var/log/syslog

You can add the script to your crontab to make it run at set intervals.

e.g. to make it run at the top of every hour you could type the following into a terminal:


crontab -e

This will open your user specific crontab. Add a line as follows:


0 * * * * /usr/bin/python /path/to/your/script.py


2 comments:

  1. Hi Tom,

    Myself and my brother have just finished building a free DDNS service http://duckdns.org with full install instructions for raspberry pi http://www.duckdns.org/install.jsp?tab=pi

    It's completely free, uses Google open auth V2 and is hosted on EC2.

    I thought you may be interested

    Regards

    ReplyDelete
  2. hi, tom
    i have written another python program, that updates freedns.afraid.org servers with the raspberry pi's external ip address and prints log files with complete info required.
    find it on http://goo.gl/RqOnnJ . its completly free and doesnt need any authetication protocol. it uses the direct update url as shown above.
    regards

    ReplyDelete