#!/usr/bin/python import os import sys import re import argparse from subprocess import Popen, PIPE, check_call, CalledProcessError import ipaddr import dns.zone # http://www.dnspython.org/ def sync_dynamic_zones(): check_call(['rndc', 'sync']) def ipfamily_by_ip(ip): if isinstance(ip, ipaddr.IPv4Address): return 'A' elif isinstance(ip, ipaddr.IPv6Address): return 'AAAA' assert False def forward_lookup(fqdn, ip_family): """Returns the ip address of the fqdn or None if none is found.. :param fqdn: Fully qualified domain name. :param ip_family: 'A' or 'AAAA'""" filename = '/var/cache/bind/dyn.colgarra.priv.at' zonename = os.path.basename(filename) zone = dns.zone.from_file(filename, zonename, relativize=False) for name, ttl, rdata in zone.iterate_rdatas(ip_family): if str(name)[:-1] == fqdn: # [:-1] removes trailing dot return ipaddr.IPAddress(rdata.address) def reverse_lookup(ip): """Returns an iterator of fqdns for the given IP address. :param ip: Instance of ipaddr.IPv4Address or ipaddr.IPv6Address""" filename = '/var/cache/bind/dyn.colgarra.priv.at' zonename = os.path.basename(filename) zone = dns.zone.from_file(filename, zonename, relativize=False) for name, ttl, rdata in zone.iterate_rdatas(ipfamily_by_ip(ip)): if ipaddr.IPAddress(rdata.address) == ip: yield str(name) def nsupdate_add(fqdn, ttl, ip): """ :param fqdn: Fully qualified domain name :param ip_family: A or AAAA :raises an NsupdateError in case of errors.""" command = "update add {fqdn} {ttl} IN {ip_family} {ip}\n\n".format(fqdn=fqdn, ttl=ttl, ip_family=ipfamily_by_ip(ip), ip=ip) p = Popen(['nsupdate', '-l'], stdin=PIPE) p.communicate(command) if p.returncode != 0: raise CalledProcessError(p.returncode, 'nsupdate -l') def nsupdate_delete(fqdn, ip_family): """ :param fqdn: Fully qualified domain name :param ip_family: A or AAAA :raises an NsupdateError in case of errors.""" command = "update delete {fqdn} {ip_family}\n\n".format(fqdn=fqdn, ip_family=ip_family) p = Popen(['nsupdate', '-l'], stdin=PIPE) p.communicate(command) if p.returncode != 0: raise CalledProcessError(p.returncode, 'nsupdate -l') def blockip_whitelist_add(ip): """ :param ip: ipv4 address :raises a CalledProcessError in case of errors.""" if ipfamily_by_ip(ip) == 'A': command = ['iptables', '-I', 'blockip', '-s', str(ip), '-j', 'ACCEPT'] check_call(command) def blockip_whitelist_delete(ip): """ :param ip: ipv4 address :raises a CalledProcessError in case of errors.""" if ipfamily_by_ip(ip) == 'A': command = ['iptables', '-D', 'blockip', '-s', str(ip), '-j', 'ACCEPT'] check_call(command) def main(args): try: if args.delete: if args.ip is None: nsupdate_delete(args.fqdn, 'A') nsupdate_delete(args.fqdn, 'AAAA') else: ipfamily = ipfamily_by_ip(args.ip) sync_dynamic_zones() old_ip = forward_lookup(args.fqdn, ipfamily) nsupdate_delete(args.fqdn, ipfamily_by_ip(args.ip)) if old_ip is not None: blockip_whitelist_delete(old_ip) else: ipfamily = ipfamily_by_ip(args.ip) sync_dynamic_zones() old_ip = forward_lookup(args.fqdn, ipfamily) nsupdate_delete(args.fqdn, ipfamily) nsupdate_add(args.fqdn, args.ttl, args.ip) if old_ip != args.ip: if old_ip is not None: blockip_whitelist_delete(old_ip) blockip_whitelist_add(args.ip) except CalledProcessError as e: sys.exit(e.returncode) if __name__ == '__main__': parser = argparse.ArgumentParser(description='Add or delete a domain name from dyndns (simplifies call to nsupdate).') parser.add_argument('-d', '--delete', action='store_true', help='delete instead of add') parser.add_argument('-i', '--ip', help='IP address (either IPv4 or IPv6)') parser.add_argument('-t', '--ttl', type=int, default=600, help='TTL (default: 600)') parser.add_argument('fqdn', help='fully qualified domain name to add or delete, e.g. myserver.dyn.example.com') args = parser.parse_args() # check ip if not args.delete and not args.ip: parser.error('The IP address is mandatory') if args.ip: try: args.ip = ipaddr.IPAddress(args.ip) # throws an exception if the IP address is not valid except ValueError: parser.error('The IP address is not valid') # check fqdn if re.match(r'[-0-9a-z]+(\.[-0-9a-z]+)*$', args.fqdn) is None: parser.error('The fqdn has an invalid format.') main(args)