Skip to content

Instantly share code, notes, and snippets.

@pklaus
Last active September 17, 2021 12:42
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save pklaus/4707775 to your computer and use it in GitHub Desktop.
Save pklaus/4707775 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)
@Foxtrod89
Copy link

thx a lot Klaus

@gilgongo
Copy link

import urllib.request is now replaced by simply import urllib as per this explanation.

@unquietwiki
Copy link

unquietwiki commented Oct 30, 2018

@pklaus Thank you for this. I found your write-up at https://blog.philippklaus.de/2013/01/updating-dns-entries-with-nsupdate-or-alternative-implementations-your-own-ddns & was lead here.

  • The first line should read #!/usr/bin/python3 on Ubuntu systems.
  • I'm trying to help out the Zentyal folks enable IPv6 on their open-source AD system. I'm testing this out to replace nsupdate usage, but didn't see any licensing attached. Please review and advise as able. Thanks! zentyal/zentyal#1822

EDIT: looks like https://gist.github.com/pklaus/4619865 might work better for Zentyal usage. Still need clarification on licensing. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment