6 from subprocess import Popen, PIPE, check_call, CalledProcessError
8 import dns.zone # http://www.dnspython.org/
11 def sync_dynamic_zones():
12 check_call(['rndc', 'sync'])
15 def ipfamily_by_ip(ip):
16 if isinstance(ip, ipaddr.IPv4Address):
18 elif isinstance(ip, ipaddr.IPv6Address):
23 def forward_lookup(fqdn, ip_family):
24 """Returns the ip address of the fqdn or None if none is found..
26 :param fqdn: Fully qualified domain name.
27 :param ip_family: 'A' or 'AAAA'"""
28 filename = '/var/cache/bind/dyn.colgarra.priv.at'
29 zonename = os.path.basename(filename)
30 zone = dns.zone.from_file(filename, zonename, relativize=False)
31 for name, ttl, rdata in zone.iterate_rdatas(ip_family):
32 if str(name)[:-1] == fqdn: # [:-1] removes trailing dot
33 return ipaddr.IPAddress(rdata.address)
36 def reverse_lookup(ip):
37 """Returns an iterator of fqdns for the given IP address.
39 :param ip: Instance of ipaddr.IPv4Address or ipaddr.IPv6Address"""
40 filename = '/var/cache/bind/dyn.colgarra.priv.at'
41 zonename = os.path.basename(filename)
42 zone = dns.zone.from_file(filename, zonename, relativize=False)
43 for name, ttl, rdata in zone.iterate_rdatas(ipfamily_by_ip(ip)):
44 if ipaddr.IPAddress(rdata.address) == ip:
48 def nsupdate_add(fqdn, ttl, ip):
50 :param fqdn: Fully qualified domain name
51 :param ip_family: A or AAAA
52 :raises an NsupdateError in case of errors."""
53 command = "update add {fqdn} {ttl} IN {ip_family} {ip}\n\n".format(fqdn=fqdn, ttl=ttl, ip_family=ipfamily_by_ip(ip), ip=ip)
54 p = Popen(['nsupdate', '-l'], stdin=PIPE)
55 p.communicate(command)
57 raise CalledProcessError(p.returncode, 'nsupdate -l')
59 def nsupdate_delete(fqdn, ip_family):
61 :param fqdn: Fully qualified domain name
62 :param ip_family: A or AAAA
63 :raises an NsupdateError in case of errors."""
64 command = "update delete {fqdn} {ip_family}\n\n".format(fqdn=fqdn, ip_family=ip_family)
65 p = Popen(['nsupdate', '-l'], stdin=PIPE)
66 p.communicate(command)
68 raise CalledProcessError(p.returncode, 'nsupdate -l')
71 def blockip_whitelist_add(ip):
73 :param ip: ipv4 address
74 :raises a BlockipError in case of errors."""
75 if ipfamily_by_ip(ip) == 'A':
76 command = ['iptables', '-I', 'blockip', '-s', str(ip), '-j', 'ACCEPT']
80 def blockip_whitelist_delete(ip):
82 :param ip: ipv4 address
83 :raises a BlockipError in case of errors."""
84 if ipfamily_by_ip(ip) == 'A':
85 command = ['iptables', '-D', 'blockip', '-s', str(ip), '-j', 'ACCEPT']
93 nsupdate_delete(args.fqdn, 'A')
94 nsupdate_delete(args.fqdn, 'AAAA')
96 ipfamily = ipfamily_by_ip(args.ip)
98 old_ip = forward_lookup(args.fqdn, ipfamily)
99 nsupdate_delete(args.fqdn, ipfamily_by_ip(args.ip))
100 if old_ip is not None:
101 blockip_whitelist_delete(old_ip)
103 ipfamily = ipfamily_by_ip(args.ip)
105 old_ip = forward_lookup(args.fqdn, ipfamily)
106 nsupdate_delete(args.fqdn, ipfamily)
107 nsupdate_add(args.fqdn, args.ttl, args.ip)
108 if old_ip != args.ip:
109 if old_ip is not None:
110 blockip_whitelist_delete(old_ip)
111 blockip_whitelist_add(args.ip)
112 except CalledProcessError as e:
113 sys.exit(e.returncode)
117 if __name__ == '__main__':
118 parser = argparse.ArgumentParser(description='Add or delete a domain name from dyndns (simplifies call to nsupdate).')
119 parser.add_argument('-d', '--delete', action='store_true', help='delete instead of add')
120 parser.add_argument('-i', '--ip', help='IP address (either IPv4 or IPv6)')
121 parser.add_argument('-t', '--ttl', type=int, default=600, help='TTL (default: 600)')
122 parser.add_argument('fqdn', help='fully qualified domain name to add or delete, e.g. myserver.dyn.example.com')
123 args = parser.parse_args()
126 if not args.delete and not args.ip:
127 parser.error('The IP address is mandatory')
130 args.ip = ipaddr.IPAddress(args.ip) # throws an exception if the IP address is not valid
132 parser.error('The IP address is not valid')
135 if re.match(r'[-0-9a-z]+(\.[-0-9a-z]+)*$', args.fqdn) is None:
136 parser.error('The fqdn has an invalid format.')