Skip to content

Instantly share code, notes, and snippets.

@enygren
Forked from pklaus/ddns.py
Created August 26, 2016 17:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save enygren/bfdeccdf262bfb0e5eda891744ade723 to your computer and use it in GitHub Desktop.
Save enygren/bfdeccdf262bfb0e5eda891744ade723 to your computer and use it in GitHub Desktop.
A script to update the A and AAAA RRs of HOSTNAME on a DNS server according to your current external IP address using nsupdate / TSIG. This script is tested to run on Mac OS X (10.8-10.9) but you should be able to get it up and running in almost no time on any Unix or Linux system that ships nsupdate. I use this script to update my own DDNS serv…
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Written on 2013-02-04 by Philipp Klaus <philipp.l.klaus →AT→ web.de>.
Check <https://gist.github.com/4707775> for newer versions.
Uses dnspython: install with `pip install dnspython3`
"""
import argparse, sys
import urllib.request
import ipaddress
import subprocess
import socket
origGetAddrInfo = socket.getaddrinfo
VERBOSE = False
IPV6_GET = "http://v6.ipv6-test.com/api/myip.php"
IPV4_GET = "http://v4.ipv6-test.com/api/myip.php"
class NoConnectivity(Exception):
pass
def get_external_IP(get_ipv6=False):
# replace the original socket.getaddrinfo to force IPv4/IPv6 connection
# see http://stackoverflow.com/a/6319043/183995
forced_family = socket.AF_INET6 if get_ipv6 else socket.AF_INET
def getAddrInfoWrapper(host, port, family=0, socktype=0, proto=0, flags=0):
return origGetAddrInfo(host, port, forced_family, socktype, proto, flags)
socket.getaddrinfo = getAddrInfoWrapper
try:
get_addr_url = IPV6_GET if get_ipv6 else IPV4_GET
retval = urllib.request.urlopen(get_addr_url).read()
retval = retval.decode('ascii').strip()
except:
return None
try:
ip = ipaddress.ip_address(retval)
return retval
except:
return None
def get_RR_value(FQDN, kind="A"):
import dns.resolver
try:
answer = dns.resolver.query(FQDN, kind)
if len(answer) == 0:
return None
return answer[0].to_text()
except dns.resolver.NXDOMAIN:
return None
except dns.resolver.NoAnswer:
return None
except dns.exception.DNSException:
return None
def IPs_match_current_RRs(FQDN, want_ipv4=True, want_ipv6=False):
my_IPv4 = get_external_IP(get_ipv6=False) if want_ipv4 else None
my_IPv6 = get_external_IP(get_ipv6=True) if want_ipv6 else None
if not (my_IPv4 or my_IPv6):
raise NoConnectivity
if VERBOSE: print("Current IPv4 and IPv6 addresses: %s and %s." % (my_IPv4, my_IPv6))
cur_RR_v4 = get_RR_value(FQDN, 'A')
cur_RR_v6 = get_RR_value(FQDN, 'AAAA')
if VERBOSE: print("Current IPv4 and IPv6 RRs in DNS: %s and %s." % (cur_RR_v4, cur_RR_v6))
return (my_IPv4 == cur_RR_v4 and my_IPv6 == cur_RR_v6)
def update_dns(server, zone, keyfile, domain, ipv4=None, ipv6=None, TTL=30):
command = "server %s\n" % server
command += "zone %s\n" % zone
command += "update delete %s A\n" % domain
command += "update delete %s AAAA\n" % domain
if ipv4:
command += "update add %s %s A %s\n" % (domain, TTL, ipv4)
if ipv6:
command += "update add %s %s AAAA %s\n" % (domain, TTL, ipv6)
command += "show\nsend\n"
command = "nsupdate -k {0} -v << EOF\n{1}\nEOF\n".format(keyfile, command)
if VERBOSE: print("Calling the following command now:\n\n" + command)
subprocess.call(command, shell=True)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Update the RRs of HOSTNAME on a DNS server according to your actual ones.')
parser.add_argument('--verbose', '-v', action='store_true',
help='Create more detailed output.')
parser.add_argument('--ipv6', '-6', action="store_true",
help='Set IPv4 entry (RR type A).')
parser.add_argument('--ipv6-getter-url', metavar='URL',
help='A URL that returns your IPv6 address upon a GET request.')
parser.add_argument('--ipv4', '-4', action="store_true",
help='Set IPv6 entry (RR type AAAA).')
parser.add_argument('--ipv4-getter-url', metavar='URL',
help='A URL that returns your IPv4 address upon a GET request.')
parser.add_argument('--zone', '-z', metavar='ZONE',
help='The zone of the host to update.', required=True)
parser.add_argument('--keyfile', '-k', metavar='DNSSEC-file.key',
help='The key file for TSIG.', required=True)
parser.add_argument('--nameserver', '-n', metavar='NS',
help='The name server to update.', required=True)
parser.add_argument('host', metavar='HOSTNAME',
help='The hostname you want updated.')
args = parser.parse_args()
if not (args.ipv4 or args.ipv6): args.ipv4 = True
if args.ipv6_getter_url:
IPV6_GET = args.ipv6_getter_url
if args.ipv4_getter_url:
IPV4_GET = args.ipv4_getter_url
if args.verbose: VERBOSE = True
try:
match = IPs_match_current_RRs(args.host, want_ipv4=args.ipv4, want_ipv6=args.ipv6)
except NoConnectivity:
if VERBOSE: sys.stderr.write("Currently no external IP could be detected. Are you connected in some way?\n")
sys.exit(1)
if not match:
if VERBOSE: print("The current IPs and the RRs do not match. Updating the DNS server...")
new_ipv4 = get_external_IP(get_ipv6=False) if args.ipv4 else None
new_ipv6 = get_external_IP(get_ipv6=True) if args.ipv6 else None
update_dns(args.nameserver, args.zone, args.keyfile, args.host, ipv4=new_ipv4, ipv6=new_ipv6)
else:
if VERBOSE: print("The current IPs and the RRs are matching. Everyting OK.")
sys.exit(0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment