Now the IP address is only deleted from the blockip whitelist when it is not used...
[toast/tdyndns.git] / bin / tdyndns_update
1 #!/usr/bin/python
2 import os
3 import sys
4 import re
5 import argparse
6 from subprocess import Popen, PIPE, check_call, CalledProcessError
7 import ipaddr
8 import dns.zone  # http://www.dnspython.org/
9
10
11 def sync_dynamic_zones():
12         check_call(['rndc', 'sync'])
13
14
15 def ipfamily_by_ip(ip):
16         if isinstance(ip, ipaddr.IPv4Address):
17                 return 'A'
18         elif isinstance(ip, ipaddr.IPv6Address):
19                 return 'AAAA'
20         assert False
21
22
23 def reverse_lookup(ip):
24         """Returns an iterator of fqdns for the given IP address.
25
26         :param ip: Instance of ipaddr.IPv4Address or ipaddr.IPv6Address"""
27         filename = '/var/cache/bind/dyn.colgarra.priv.at'
28         zonename = os.path.basename(filename)
29         zone = dns.zone.from_file(filename, zonename, relativize=False)
30         for name, ttl, rdata in zone.iterate_rdatas(ipfamily_by_ip(ip)):
31                 if ipaddr.IPAddress(rdata.address) == ip:
32                         yield str(name)
33
34
35 def nsupdate_add(fqdn, ttl, ip):
36         """
37         :param fqdn: Fully qualified domain name
38         :param ip_family: A or AAAA
39         :raises an NsupdateError in case of errors."""
40         command = "update add {fqdn} {ttl} IN {ip_family} {ip}\n\n".format(fqdn=fqdn, ttl=ttl, ip_family=ipfamily_by_ip(ip), ip=ip)
41         p = Popen(['nsupdate', '-l'], stdin=PIPE)
42         p.communicate(command)
43         if p.returncode != 0:
44                 raise CalledProcessError(p.returncode, 'nsupdate -l')
45
46 def nsupdate_delete(fqdn, ip_family):
47         """
48         :param fqdn: Fully qualified domain name
49         :param ip_family: A or AAAA
50         :raises an NsupdateError in case of errors."""
51         command = "update delete {fqdn} {ip_family}\n\n".format(fqdn=fqdn, ip_family=ip_family)
52         p = Popen(['nsupdate', '-l'], stdin=PIPE)
53         p.communicate(command)
54         if p.returncode != 0:
55                 raise CalledProcessError(p.returncode, 'nsupdate -l')
56
57
58 def blockip_whitelist_add(ip):
59         """
60         :param ip: ipv4 address
61         :raises a BlockipError in case of errors."""
62         command = ['iptables', '-I', 'blockip', '-s', str(ip), '-j', 'ACCEPT']
63         check_call(command)
64
65
66 def blockip_whitelist_delete(ip):
67         """
68         :param ip: ipv4 address
69         :raises a BlockipError in case of errors."""
70         command = ['iptables', '-D', 'blockip', '-s', str(ip), '-j', 'ACCEPT']
71         check_call(command)
72
73
74 def main(args):
75         try:
76                 if args.delete:
77                         if args.ip is None:
78                                 nsupdate_delete(args.fqdn, 'A')
79                                 nsupdate_delete(args.fqdn, 'AAAA')
80                         else:
81                                 nsupdate_delete(args.fqdn, ipfamily_by_ip(args.ip))
82                                 if ipfamily_by_ip(args.ip) == 'A':
83                                         sync_dynamic_zones()
84                                         if len(list(reverse_lookup(args.ip))) == 0:
85                                                 blockip_whitelist_delete(args.ip)
86                 else:
87                         nsupdate_delete(args.fqdn, ipfamily_by_ip(args.ip))
88                         nsupdate_add(args.fqdn, args.ttl, args.ip)
89                         if ipfamily_by_ip(args.ip) == 'A':
90                                 blockip_whitelist_add(args.ip)
91         except CalledProcessError as e:
92                 sys.exit(e.returncode)
93
94
95
96 if __name__ == '__main__':
97         parser = argparse.ArgumentParser(description='Add or delete a domain name from dyndns (simplifies call to nsupdate).')
98         parser.add_argument('-d', '--delete', action='store_true', help='delete instead of add')
99         parser.add_argument('-i', '--ip', help='IP address (either IPv4 or IPv6)')
100         parser.add_argument('-t', '--ttl', type=int, default=600, help='TTL (default: 600)')
101         parser.add_argument('fqdn', help='fully qualified domain name to add or delete, e.g. myserver.dyn.example.com')
102         args = parser.parse_args()
103
104         # check ip
105         if not args.delete and not args.ip:
106                 parser.error('The IP address is mandatory')
107         if args.ip:
108                 try:
109                         args.ip = ipaddr.IPAddress(args.ip) # throws an exception if the IP address is not valid
110                 except ValueError:
111                         parser.error('The IP address is not valid')
112
113         # check fqdn
114         if re.match(r'[-0-9a-z]+(\.[-0-9a-z]+)*$', args.fqdn) is None:
115                 parser.error('The fqdn has an invalid format.')
116
117         main(args)