Recently I received a notification that DynDNS is terminating their free dynamic DNS (DDNS) resolution service in favor of a paid account. DynDNS has been a wonderful company, and I have enjoyed using their free services for almost a decade. Now that they are no longer supporting free accounts, I felt I knew enough about Internet infrastructure to set up my own dynamic DNS service.

First, a little background on what dynamic DNS (DDNS) is. If you are familiar with DDNS, you can skip ahead to the next paragraph. DNS stands for Domain Name System. All domain names, such as komputerwiz.net or google.com and their subdomains, such as matthew.komputerwiz.net, are really memorable aliases for IP addresses (e.g. 50.56.79.101). The process by which a domain name is translated into an IP address is like looking up a word in the index of a book: each entry in the index references page numbers, alternate sub-phrases containing the word, or references to other words in the index. The index of a book may also be an entirely separate book (such as the World Book encyclopedia series). In this analogy, the index is a name server, which contains DNS entries for all the domains for which it is an authority 1; the word is the domain name; and the page number is the IP address associated with that name. In practice, only one address or reference to another domain name may be associated with a DNS entry. In Dynamic DNS, imagine if a page changes its position in the book over time. If the entries in the index are to remain accurate, the page numbers next to all the corresponding entries need to be changed every time the page changes position. Dynamic DNS is the process by which (pardon the analogy stretch) the page tells the index its new page number. Thus, looking up the IP address for a server whose IP address changes periodically is like hitting a moving target.

In setting up a DDNS server, I knew I would need a server behind a dynamic IP address (henceforth called “target”) and a server with a static IP address (henceforth called “sniper”). My cloud server on Rackspace shall play the role of sniper, and my server at home shall play the role of target.

In my misguided first attempt, I set up a DNS server on sniper that could be updated via the nsupdate command. I set up a script that could be triggered remotely via a secure HTTP request, and so I could update the DNS records on sniper by periodically sending an HTTP request from target. The problem I encountered was joining the DNS records that pointed to sniper with the records in sniper’s DNS server: I could resolve target to an IP address if I ran dig or nslookup on sniper, but I could not look up target’s IP address from outside the network.

After speaking with Rackspace’s immensely awesome, fanatical support team, I realized the error of my ways and found a much better and easier solution: the Rackspace Cloud DNS API (cue full-spread angelic chords 2 ). With this solution, there is no server-side handler required. I just have a script on target that periodically checks if its external IP address has changed. If the IP address has changed, it performs the necessary API calls to update the cloud DNS record:

#!/usr/bin/env python
# -*- coding: utf-8 -*-


import datetime
import dateutil.parser
import json
import os.path
import pytz
import requests


ConfigFile = '/path/to/ddnsupdate.config.json'
CacheFile = '/path/to/ddnsupdate.cache.json'


with open(ConfigFile, 'r') as f:
    config = json.load(f)


with open(CacheFile, 'r') as f:
    cache = json.load(f)


# get IP address
ip = requests.get('http://icanhazip.com').text.strip()
if ip == cache['ip']:
    print 'IP address is still ' + ip + '; nothing to update.'
    exit()


cache['ip'] = ip


now = datetime.datetime.now(pytz.utc)
expires = dateutil.parser.parse(cache['auth']['expires'])


if expires <= now:
    print 'Expired authentication token; reauthenticating...'


    # authenticate with Rackspace
    authUrl = 'https://identity.api.rackspacecloud.com/v2.0/tokens'


    authData = {
        'auth': {
            'RAX-KSKEY:apiKeyCredentials': {
                'username': config['username'],
                'apiKey': config['api_key']
            }
        }
    }


    authHeaders = {
        'Accept': 'application/json',
        'Content-type': 'application/json'
    }


    auth = requests.post(authUrl, data=json.dumps(authData), headers=authHeaders).json
    cache['auth']['expires'] = auth['access']['token']['expires']
    cache['auth']['token'] = auth['access']['token']['id']


# update DNS record
url = 'https://dns.api.rackspacecloud.com/v1.0/' + config['account_id'] + \
    '/domains/' + config['domain_id'] + '/records/' + config['record']['id']


data = {
    'ttl': config['record']['ttl'],
    'name': config['record']['name'],
    'data': cache['ip']
}


headers = {
    'Accept': 'application/json',
    'Content-type': 'application/json',
    'X-Auth-Token': cache['auth']['token']
}


result = requests.put(url, data=json.dumps(data), headers=headers)


print 'Updated IP address to ' + cache['ip']


with open(CacheFile, 'w') as f:
    json.dump(cache, f)

Update 11/10/2014: Here is a sample configuration:

{
    "username": "YOUR_USERNAME",
    "api_key": "YOUR_API_KEY",
    "account_id": "123456789",
    "domain_id": "123456789",
    "record_id": "A-123456789"
}

  1. For example, the net. authority only stores information about domain names ending in .net↩︎

  2. For the musically inclined, the chord progression in that YouTube video goes something like E-B-E♭maj7/-Cm-G-D-E5-C ↩︎