summaryrefslogtreecommitdiff
path: root/he-dyndns
blob: 443e76e17786638dcc149e16afcd4de4b144dc6c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/usr/bin/env python3

import argparse
import base64
import configparser
import dns.resolver
import ipaddress
import logging
import logging.handlers
import os.path
import socket
import sys
import urllib


HE_DNS_API_HOST = "dyn.dns.he.net"

def build_ip(ip_string):
	try:
		ip = ipaddress.ip_address(ip_string)
	except ValueError:
		log.error("manually specified address is invalid format: %s" % ip_string)
		sys.exit(1)
	except:
		log.error(str(e))
		sys.exit(1)

	return ip

def haveIPv6():
	haveIPv6 = True
	try:
		s = socket.socket(socket.AF_INET6)
		s.connect((HE_DNS_API_HOST,80))
	except socket.gaierror as e:
		log.debug("Exception {}".format(e.__class__))
		log.error("Testing IPv6 connection: " + str(e))
		sys.exit(1)
	except OSError as e:
		log.debug("Exception {}".format(e.__class__))
		log.debug(str(e))
		haveIPv6 = False
	except TimeoutError as e:
		log.error("Testing IPv6 connection: " + str(e))
		sys.exit(1)
	except Exception as e:
		log.debug("Exception {}".format(e.__class__))
		log.error(str(e))
		haveIPv6 = False
	return haveIPv6

def getKey(keyfile, record):
	if not os.path.exists(keyfile):
		raise Exception("missing keyfile {}".format(keyfile))
	
	config = configparser.ConfigParser()
	config.read(keyfile)

	if not "keys" in config:
		raise Exception("keyfile lacks [keys] section")

	if not record in config["keys"]:
		raise Exception("keyfile has no entry for {}".format(record))

	return config["keys"][record]

def main():
	ap = argparse.ArgumentParser(description="Update Hurricane Electric DNS dynamic record")
	ap.add_argument("record", help="DNS record to update")
	ap.add_argument("--v4", help="Update IPv4 A record (default)", action="store_true", default=True)
	ap.add_argument("--v6", help="Update IPv6 AAAA record", action="store_true", default=False)
	ap.add_argument("--addr4", help="Update A record with provided IP rather than detected IP")
	ap.add_argument("--addr6", help="Update AAAA record with provided IP rather than detected IP")
	ap.add_argument("--keyfile", help="Alternate location for key config file (default /etc/he-dns-secret.conf)")
	ap.add_argument("--key", help="HE DDNS key for record (by default read from /etc/he-dns-secret.conf or --keyfile)")
	ap.add_argument("--debug", help="Enable debug logging", action="store_true", default=False)
	args = ap.parse_args()

	if args.debug:
		log.setLevel(logging.DEBUG)
	else:
		log.setLevel(logging.ERROR)

	if args.keyfile:
		log.debug("KEYFILE: {}".format(args.keyfile))
		keyfile = args.keyfile
	else:
		log.debug("KEYFILE: /etc/he-dyndns.conf")
		keyfile = "/etc/he-dyndns.conf"

	if args.addr4:
		ip = build_ip(args.addr4)	

	if args.addr6:
		ip = build_ip(args.addr6)

	if args.v6:
		if not haveIPv6  and not args.addr6:
			log.error("no IPv6 detected and --v6 specified without --addr6 (try --debug?)")
			sys.exit(1)			

	try:

		apikey = getKey(keyfile, args.record)
		
		if args.v6:
			ans = dns.resolver.resolve(HE_DNS_API_HOST, "AAAA")[0]
			log.debug("DNS resolved: {}".format(ans.to_text()))
			apihost = "[{}]".format(ans.to_text())
		else:
			ans = dns.resolver.resolve(HE_DNS_API_HOST, "A")[0]
			log.debug("DNS resolved: {}".format(ans.to_text()))
			apihost = ans.to_text()

		# build the API URL to be called
		url = "http://{}/nic/update?hostname={}".format(apihost, args.record)
		if args.addr4 or args.addr6:
			url += "&myip={}".format(ip)
		log.debug("API request URL: " + url)

		# make the HTTP request
		authstr = base64.b64encode(bytes("%s:%s" % ( args.record, apikey ), "ascii"))
		log.debug("auth string: %s" % authstr.decode("utf-8"))
		req = urllib.request.Request(url)
		req.add_header("Host", HE_DNS_API_HOST)
		req.add_header("Authorization", "Basic %s" % authstr.decode("utf-8"))
		with urllib.request.urlopen(req) as response:
			this_page = response.read()
		log.debug("HE response: " + this_page.decode("utf-8"))
		res = this_page.decode("utf-8").split()[0]

		if res == "good":
			return True
		elif res == "badauth":
			raise Exception("HE badauth: wrong key or record not configured for dynamic updates")
		elif res == "nochg":
			return True
		else:
			raise Exception("HE returned {}: unknown response code".format(res))

	except dns.resolver.NXDOMAIN as e:
		log.debug("Exception class: {}".format(e.__class__))
		log.error(str(e))
		sys.exit(1)

	except Exception as e:
		log.debug("Exception class: {}".format(e.__class__))
		log.error(str(e))    
		sys.exit(1)

if __name__ == "__main__":
	log = logging.getLogger("he-dyndns")
	lh = logging.StreamHandler()
	lf = logging.Formatter(fmt='%(name)s: %(levelname)s: %(message)s')
	lh.setFormatter(lf)
	log.addHandler(lh)
	main()
	sys.exit(0)