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
- 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==
-
Name the key for example
ddns_keyand put the following into your/var/named/named.keysfile:key ddns_key { algorithm hmac-md5; secret "7i3+IXiKmU00jA4f8VWHwA=="; }; - Finally configure your zones (aka domains) for dynamic updates. To
do that open
/var/named/named.confand 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; }; }; -
Make the
masters/directory anddb.example.comfile owned by the user runningnamedprocess (usually usernamed), 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
-
Now restart the nameserver:
~# /etc/init.d/named restart Stopping named: [ OK ] Starting named: [ OK ]
And that's it. -
Now you're ready to test the ddns-updater. By default it uses
/var/named/named.keysfor authentication but feel free to change the path to yournamed.keysfile in the config section near the top of theddns-updaterscript.~# 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.
-
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')