DDNS-UPDATER.TXT

Programs and patches

DDNS updater

Command-line forward and reverse dynamic-DNS updater.

Last updated · 16 April 2008

Easy to use command line utility for creating and updating forward and revers DNS entries in dynamically updatable domains.

Supports zones on different servers, supports different keys for each zone, automatically creates reverse record and removes obsoleted ones.

Example use

~$ ddns-updater  server.example.com  192.0.2.1

will create a forward A record in example.com zone:

  server  3600  IN  A  192.0.2.1

and a reverse PTR record in 2.0.192.in-addr.arpa zone:

  1       3600  IN  A  server.example.com.

Server config

The zone you're about to manage must have dynamic updates enabled. It should also require a key to authorize the updates. For bind 9 nameserver the config would be similar to this example, assuming you have nameserver config files in /var/named

  1. Create an authentication key with dnssec-keygen:
    ~$ dnssec-keygen -a HMAC-MD5 -n HOST -b 512 example.com
    Kexample.com.+157+39941
    ~$ cat Kexample.com.+157+39941.private
    Private-key-format: v1.2
    Algorithm: 157 (HMAC_MD5)
    Key: 7i3+IXiKmU00jA4f8VWHwA==
    
  2. Name the key for example ddns_key and put the following into your /var/named/named.keys file:
    key ddns_key {
    	algorithm hmac-md5;
    	secret "7i3+IXiKmU00jA4f8VWHwA==";
    };
    
  3. Finally configure your zones (aka domains) for dynamic updates. To do that open /var/named/named.conf and add the following:
    options { ... };
    
    include "/var/named/named.keys";
    
    zone "example.com" {
    	type master;
    	file "masters/db.example.com";
    	allow-update { key ddns_key; };
    };
    
    zone "2.0.192.in-addr.arpa" {
    	type master;
    	file "masters/rev.192.0.2";
    	allow-update { key ddns_key; };
    };
    
  4. Make the masters/ directory and db.example.com file owned by the user running named process (usually user named), assuming they're both writable for owner:
    ~# chown named /var/named/masters
    ~# chown named /var/named/masters/db.example.com
    ~# chown named /var/named/masters/rev.192.0.2
    
  5. Now restart the nameserver:
    ~# /etc/init.d/named restart
    Stopping named:      [  OK  ]
    Starting named:      [  OK  ]
    
    And that's it.
  6. Now you're ready to test the ddns-updater. By default it uses /var/named/named.keys for authentication but feel free to change the path to your named.keys file in the config section near the top of the ddns-updater script.
    ~# ddns-updater server.example.com 192.0.2.1
    Command:  /usr/bin/nsupdate -y ddns_key:7i3+IXiKmU00jA4f8VWHwA== /tmp/tmpfile-BwSfIW
    Outgoing update query:
    ;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id:      0
    ;; flags: ; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
    ;; UPDATE SECTION:
    server.example.com.        0       ANY     ANY
    server.example.com.        3600    IN      A       192.0.2.1
    
    Command:  /usr/bin/nsupdate -y ddns_key:7i3+IXiKmU00jA4f8VWHwA== /tmp/tmpfile-au9QHP
    Outgoing update query:
    ;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id:      0
    ;; flags: ; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
    ;; UPDATE SECTION:
    1.2.0.192.in-addr.arpa. 0  ANY     ANY
    1.2.0.192.in-addr.arpa. 3600 IN    PTR     server.example.com.
    
  7. The last thing is to verify that the zone has been updated:
    ~$ host server.example.com
    server.example.com has address 192.0.2.1
    ~$ host 192.0.2.1
    1.2.0.192.in-addr.arpa domain name pointer server.example.com.
    
    Excellent! That's it ;-)

ToDo

  • support for config file
  • IPv6 addresses support
  • support for other types of records (CNAME, MX, etc)
  • ... anyone keen to help with these items? Let me know ;-)

Download

Download the script, put your settings in its header and make it executable
Read the script below · Download raw ddns-updater.py

ddns-updater source


#!/usr/bin/env python

## DDNS (Dynamic DNS) zone updater
## 
## Easy to use command line utility for creating
## and updating forward and reverse DNS entries
## in dynamically updatable domains.
## 
## Supports zones on different servers, supports 
## different keys for each zone, automatically 
## creates reverse record and removes obsoleted
## ones.
## 
## Example use:
## $ ddns-updater  server.example.net  192.0.2.1
## will create a forward A record in example.net zone:
##   server  3600  IN  A  192.0.2.1
## and a reverse PTR record in 2.0.192.in-addr.arpa zone:
##   1       3600  IN  A  server.example.net.
## 
## Author: Michal Ludvig <michal@logix.cz>
##         /devel/ddns-updater
## 
## License: GPL version 2
##
## TODO: 
## * support for config file
## * IPv6 addresses support
## * support for other types of records (CNAME, MX, etc)
## ... anyone keen to help with these items?  ;-)

import sys
import os
import random
import string
import re

ZONE_SERVER = {}
ZONE_KEY = {}
SERVER_KEY = {}
TTL_DEFAULT = 3600
NSUPDATE_PATH = "/usr/bin/nsupdate"

### ------ Put your local config here ------

ZONE_SERVER['default'] = "127.0.0.1"
#ZONE_SERVER['example.com'] = "192.0.2.123"
#ZONE_SERVER['example.net'] = "192.0.2.123"
#ZONE_KEY['example.com'] = '/etc/rndc.key'
#ZONE_KEY['example.net'] = 'example_net_key:NH1/N+RvZyrf1kH7/x5+c2MsLdlzL'
SERVER_KEY['127.0.0.1'] = "/var/named/named.keys"

### ------ Nothing to configure below this line ------

_rnd_chars = string.ascii_letters+string.digits
_rnd_chars_len = len(_rnd_chars)
def rndstr(len):
        retval = ""
        while len > 0:
                retval += _rnd_chars[random.randint(0, _rnd_chars_len-1)]
                len -= 1
        return retval

def mktmpsomething(prefix, randchars, createfunc):
        old_umask = os.umask(0077)
        tries = 5
        while tries > 0:
                dirname = prefix + rndstr(randchars)
                try:
                        createfunc(dirname)
                        break
                except OSError, e:
                        if e.errno != errno.EEXIST:
                                os.umask(old_umask)
                                raise
                tries -= 1

        os.umask(old_umask)
        return dirname

def mktmpdir(prefix = "/tmp/tmpdir-", randchars = 10):
        return mktmpsomething(prefix, randchars, os.mkdir)

def mktmpfile(prefix = "/tmp/tmpfile-", randchars = 20):
        createfunc = lambda filename : os.close(os.open(filename, os.O_CREAT | os.O_EXCL))
        return mktmpsomething(prefix, randchars, createfunc)

def parse_key(key_file):
        try:
                key = open(key_file, "r").read()
                pat = re.compile('key\s+(\w+)\s*{.*secret\s+[\'"]([^\'"]+)[\'"]', re.DOTALL | re.MULTILINE)
                result = ":".join(pat.search(key).groups())
                return result
        except Exception, e:
                sys.stderr.write("Can parse a key from %s: %s\n" % (key_file, e))
                sys.exit(1)

def run_nsupdate(zone, args_in, mode):
        """
        run_nsupdate(zone, args, mode)

        zone is either forward or reverse DNS zone
        args dict must have 'hostfqdn' and 'ipv4' members
        mode is either 'forward' or 'reverse'
        """
        # Use a copy, don't update the original
        args = args_in.copy()
        args['zone'] = zone

        # Open tmp file for nsupdate script
        args['tmpfilename'] = mktmpfile()
        tmpfile = open(args['tmpfilename'], "w")

        # Figure out server IP or name
        args['server'] = None
        if ZONE_SERVER.has_key(zone):
                args['server'] = ZONE_SERVER[zone]
        elif ZONE_SERVER.has_key('default'):
                args['server'] = ZONE_SERVER['default']

        # Find a key for this zone/server
        args['key'] = None
        args['key_param'] = None # key_param is a switch for nsupdate, -k for file, -y for inline key
        if ZONE_KEY.has_key(zone):
                args['key'] = ZONE_KEY[zone]
        elif SERVER_KEY.has_key(args['server']):
                args['key'] = SERVER_KEY[args['server']]
        if args['key']:
                if os.path.isfile(args['key']):
                        args['key'] = parse_key(args['key'])
                # args['key_param'] = os.path.isfile(args['key']) and "-k" or "-y"
                args['key_param'] = "-y"

        # Generate the nsupdate script
        if args['server']:
                tmpfile.write("server %(server)s\n" % args)
        tmpfile.write("zone %(zone)s\n" % args)
        if mode == 'forward':
                tmpfile.write("update delete %(hostfqdn)s.\n" % args)
                tmpfile.write("update add %(hostfqdn)s. %(ttl)s IN A %(ipv4)s\n" % args)
        elif mode == 'reverse':
                tmpfile.write("update delete %(reverse_ipv4)s.\n" % args)
                tmpfile.write("update add %(reverse_ipv4)s. %(ttl)s IN PTR %(hostfqdn)s.\n" % args)
        tmpfile.write("show\n")
        tmpfile.write("send\n")
        tmpfile.close()

        # Run nsupdate command
        args['key_arg'] = args['key'] and "%(key_param)s %(key)s" % args or ""
        command = "%(nsupdate)s %(key_arg)s %(tmpfilename)s" % args
        print "Command: ", command
        ret = os.system(command)

        # Clean up
        os.remove(args['tmpfilename'])

if __name__ == "__main__":
        if len(sys.argv) < 3:
                sys.stderr.write("Usage: %s <hostfqdn> <ipv4-addr>\n" % sys.argv[0])
                sys.stderr.write("E.g.:  %s server.example.net 192.0.2.1\n" % sys.argv[0])
                sys.exit(1)

        hostfqdn = sys.argv[1]
        ipv4 = sys.argv[2]

        host_parts = hostfqdn.split(".", 1)
        ipv4_parts = ipv4.split(".")

        if len(host_parts) != 2:
                sys.stderr.write("Hostname '%s' is not fully qualified. Append your domain please.\n" % hostfqdn)
                sys.exit(1)

        if len(ipv4_parts) != 4:
                sys.stderr.write("Invalid IPv4 address: %s\n" % ipv4)
                sys.exit(1)

        try:
                ipv4_parts = [int(nibble) for nibble in ipv4_parts]
        except ValueError:
                sys.stderr.write("Invalid IPv4 address: %s\n" % ipv4)
                sys.exit(1)

        zone_fwd = host_parts[1]
        zone_rev = "%d.%d.%d.in-addr.arpa" % (ipv4_parts[2], ipv4_parts[1], ipv4_parts[0])

        args = {}
        args['hostfqdn'] = hostfqdn
        args['host'] = host_parts[1]
        args['ipv4'] = ipv4
        args['reverse_ipv4'] = "%d.%s" % (ipv4_parts[3], zone_rev)
        args['ttl'] = TTL_DEFAULT
        args['nsupdate'] = NSUPDATE_PATH

        run_nsupdate(zone_fwd, args, 'forward')
        run_nsupdate(zone_rev, args, 'reverse')

Download raw ddns-updater.py ↓

DDNS updaterUTF-8 · LOGIX.CZ

Find in Logix

Search page titles and descriptions. Esc closes.

A few handy keys

A little Borland, a little early desktop, a corner of the web to call home.

Pick a directory from the menu or tree. Use Find when you know what you’re after.

Tab / Shift+Tab
Move between controls
↑ / ↓ in menus
Choose an entry
← / → in menus
Switch directories
/
Find a page
Esc
Close menu or dialog
F1
Open this help

Prefer a different atmosphere? View has blue, grey, and green palettes.

No commands to learn. Just follow your curiosity.