#!/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 ## http://www.logix.cz/michal/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 \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')