27bfed0627e4e3e043cb171b821e8c3925300274
[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 forward_lookup(fqdn, ip_family):
24         """Returns the ip address of the fqdn or None if none is found..
25
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)
34
35
36 def reverse_lookup(ip):
37         """Returns an iterator of fqdns for the given IP address.
38
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:
45                         yield str(name)
46
47
48 def nsupdate_add(fqdn, ttl, ip):
49         """
50         :param fqdn: Fully qualified domain name
51         :param ip_family: A or AAAA
52         :raises an CalledProcessError 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)
56         if p.returncode != 0:
57                 raise CalledProcessError(p.returncode, 'nsupdate -l')
58
59 def nsupdate_delete(fqdn, ip_family):
60         """
61         :param fqdn: Fully qualified domain name
62         :param ip_family: A or AAAA
63         :raises an CalledProcessError 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)
67         if p.returncode != 0:
68                 raise CalledProcessError(p.returncode, 'nsupdate -l')
69
70
71 def blockip_whitelist_add(ip):
72         """
73         :param ip: ipv4 address
74         :raises a CalledProcessError in case of errors."""
75         if ipfamily_by_ip(ip) == 'A':
76                 command = ['iptables', '-I', 'blockip', '-s', str(ip), '-j', 'ACCEPT']
77                 check_call(command)
78
79
80 def blockip_whitelist_delete(ip):
81         """
82         :param ip: ipv4 address
83         :raises a CalledProcessError in case of errors."""
84         if ipfamily_by_ip(ip) == 'A':
85                 command = ['iptables', '-D', 'blockip', '-s', str(ip), '-j', 'ACCEPT']
86                 check_call(command)
87
88
89 def main(args):
90         try:
91                 if args.delete:
92                         if args.ip is None:
93                                 nsupdate_delete(args.fqdn, 'A')
94                                 nsupdate_delete(args.fqdn, 'AAAA')
95                         else:
96                                 ipfamily = ipfamily_by_ip(args.ip)
97                                 sync_dynamic_zones()
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)
102                 else:
103                         ipfamily = ipfamily_by_ip(args.ip)
104                         sync_dynamic_zones()
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)
114
115
116
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()
124
125         # check ip
126         if not args.delete and not args.ip:
127                 parser.error('The IP address is mandatory')
128         if args.ip:
129                 try:
130                         args.ip = ipaddr.IPAddress(args.ip) # throws an exception if the IP address is not valid
131                 except ValueError:
132                         parser.error('The IP address is not valid')
133
134         # check fqdn
135         if re.match(r'[-0-9a-z]+(\.[-0-9a-z]+)*$', args.fqdn) is None:
136                 parser.error('The fqdn has an invalid format.')
137
138         main(args)