#!/usr/bin/env python3 import argparse import ipaddress import netifaces import logging import logging.handlers import os.path import socket import subprocess import sys 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 get_interface_addrs(iface, addrtype): try: iaddrs = netifaces.ifaddresses(iface) i = iaddrs[addrtype] log.debug("interface {} addrs of type {}: {}".format(iface,addrtype,i)) except ValueError as e: log.debug("Exception class: {}".format(e.__class__)) log.error("No interface named {} found".format(iface)) sys.exit(1) except KeyError as e: log.debug("Exception class: {}".format(e.__class__)) log.error("No address of type {} found on interface {}".format(addrtype, iface)) sys.exit(1) except Exception as e: log.debug("Exception class: {}".format(e.__class__)) log.error(str(e)) sys.exit(1) return i def build_nsupdate_cmd(server, zone, record, rrtype, ttl, addr): log.debug("BNC: server: {}".format(server)) log.debug("BNC: zone: {}".format(zone)) log.debug("BNC: record: {}".format(record)) log.debug("BNC: rrtype: {}".format(rrtype)) log.debug("BNC: ttl: {}".format(ttl)) log.debug("BNC: addr: {}".format(addr)) cmdstr = "server {}\n".format(server) cmdstr += "zone {}\n".format(zone) cmdstr += "update delete {} {}\n".format(record, rrtype) cmdstr += "update add {} {} {} {}\n".format(record, ttl, rrtype, addr) cmdstr += "send\n" cmdstr += "quit\n" return cmdstr def main(): ap = argparse.ArgumentParser(description="Update dynamic DNS records using RFC2136") ap.add_argument("interface", help="interface to obtain IP from (for IPv6, takes the numerically first global address on the interface)") ap.add_argument("record", help="DNS record to update") ap.add_argument("zone", help="Zone name to update (e.g. example.com)") ap.add_argument("server", help="Server to update (IP or FQDN)") ap.add_argument("--v4", help="Update IPv4 A record", action="store_true", default=False) ap.add_argument("--v6", help="Update IPv6 AAAA record (default)", action="store_true", default=True) ap.add_argument("--addr4", help="Update A record with provided IP rather than detected IP (causes 'interface' to be ignored)") ap.add_argument("--addr6", help="Update AAAA record with provided IP rather than detected IP (causes 'interface' to be ignored)") ap.add_argument("--ttl", help="TTL to assign to record (default 300)", default=300) ap.add_argument("--keyfile", help="Alternate location for key config file (default /etc/ddns-RECORD.key where RECORD is the record name provided as the argument)") 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: keyfile = args.keyfile else: keyfile = "/etc/ddns-{}.key".format(args.record) log.debug("KEYFILE: {}".format(keyfile)) if args.v4: addrtype = netifaces.AF_INET else: addrtype = netifaces.AF_INET6 ip = False if args.addr4: ip = build_ip(args.addr4) if args.addr6: ip = build_ip(args.addr6) try: if not os.path.exists(keyfile): raise Exception("missing key file {}".format(keyfile)) if ip: addr = ip log.debug("Manual IP: {}".format(addr)) else: addrarr = get_interface_addrs(args.interface, addrtype) addr = addrarr.pop(0)['addr'] log.debug("Found IP: {}".format(addr)) rrtype = "AAAA" if args.v4: rrtype = "A" nsupdatecmd = build_nsupdate_cmd(args.server, args.zone, args.record, rrtype, args.ttl, addr) nsupdate = subprocess.run( "nsupdate -k {}".format(keyfile), shell=True, check=True, input = bytes(nsupdatecmd,'utf-8'), capture_output = True ) log.debug("nsupdate return code: {}".format(nsupdate.returncode)) except subprocess.CalledProcessError as e: log.debug("Exception class: {}".format(e.__class__)) log.error("Error executing: " + e.cmd) log.error("nsupdate stderr: " + e.stderr.decode("utf-8").strip()) 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("ddns-update-rfc2136") lh = logging.StreamHandler() lf = logging.Formatter(fmt='%(name)s: %(levelname)s: %(message)s') lh.setFormatter(lf) log.addHandler(lh) main() sys.exit(0)