#!/bin/bash

# Dynamically update a ISC Bind name server according to RFC2136
# by using nsupdate with TSIG keys to register or remove an IP address.

# Let this script execute from cron, from ISC dhclient as an exit hook and/or from ifupdown.
# Using cron with a short interval is recommended for dynamic, public IPs in a combination
# with other mechanisms.

# ddnsupdate [--quiet|-q|--version|-v|--help|-h|-?] [<reason> [<new_ip_address>]]
#
#		<reason> is the reason as given by ISC dhclient and additionally
#		NONDHCP and NONDHCP6. Setting a <reason> is not meant to happen
#		in normal operation.


VERSION="1.1.5"
VERSION_INFO="(c) 2013-2025 by Adrian Zaugg under GNU General Public License Version 3."


# Hostname including domain to set an IP for
HOSTNAME=""

# TTL to set in seconds
TTL=120

# Force recheck of registered address in DNS after that many minutes
# 0: recheck always; unset: recheck never
#
# Set to recheck always, if the public interface is not on this host.
FORCE_RECHECK_MINUTES=0

# Set of expected IPs as a space separated list. If the current address is not within these IPs,
# set a fixed fallback address for your host in the DNS (see below). Use numerical notation to be sure
# this works. Leave empty to disable this functionality.
EXPECTED_IPS=""

# Enter an ip you want to tell your DNS, when you don't have an expected IP. Use numerical form.
FALLBACKIP4=""
FALLBACKIP6=""

# Enter an IP you want to tell your DNS, when you don't have a connection or if the IP was withdrawn.
# Leaving empty deletes a previously associated record. Use numerical form.
DOWNIP4=""
DOWNIP6=""

# Use the public IP (e.g. when behind a NAT router) instead of DHCP assigned or fixed IP, defaults to true.
PUBLIC_IP=true

# IP service URL(s) to determine the public IP address
#
# The type of service gets detected automatically. The script expects that the service returns either a
# raw IP address string alone or a HTML page containing a line "Current IP Address: " followed by an IP address.
#
# Use a DNS name of an IP service whenever possible, that is able to return IPv4 and IPv6 addresses correspondent
# to the IP family the request was sent.
#
# You may set multiple services using bash array notation: MYIP_SERVICES=(<service1> <service2> ...)
# ... or just a single URL.
MYIP_SERVICES=(https://ip.3eck.net/ https://ip.door.ch/)

# Allow insecure connection with https to the ip service
MYIP_SERVICE_ALLOW_INSECURE_TLS=false

# name server to update
SERVER=""

# TSIG private key file for authentication on your name server
#
# Nsupdate supoorts two formats of key files: Either use the output from tsig-keygen (resp. ddns-confgen),
# which is the same format as used in your server's named.conf[.local] or use dnssec-keygen (deprecated).
#
# To generate your key using tsig-keygen:
#
#	tsig-keygen -a sha512 <name_of_key_with_trailing_dot> > <keyfile>
#		where: <name_of_key_with_trailing_dot> is best named after your dynamic host's FQDN
#
# 	Secure the generated file <keyfile> and reference it using: KEYFILE="<keyfile>"
#
#
# To generate your key using dnssec-keygen:
#
#       dnssec-keygen -a HMAC-SHA512 -b 512 -n HOST -K . <name_of_key_with_trailing_dot>
#
#               where: <name_of_key_with_trailing_dot> is best named after your dynamic host's FQDN
#
#	Both files xxx.private and xxx.key need to be next to each other. Reference the private key file below.
#
KEYFILE=""

# Path to fping: Set manually, if it isn't in your PATH.
PING=""

# Max number of ping pakets to send before giving up. Time increases exponentially,
# use a number < 7. See man fping.
RETRIES=4

# set to true for debug output
DEBUG=true



# --------- do not edit below ---------


# answer of fping to reachable hosts
ALIVE_ANSWER="is alive"

# detect help switch
if [ "$(echo -n " $@ " | grep -c -E '^.* -h .*$|^.* -\? .*$|^.* --help .*$')" -gt 0 ]; then
    echo
    echo "Dynamically update a ISC Bind name server according to RFC2136 by using nsupdate with"
    echo "TSIG keys to register or remove an IP address."
    echo
    echo
    echo "	ddnsupdate [--quiet|-q|--version|-v|--help|-h|-?] [<reason> [<new_ip_address>]]"
    echo
    echo "		<reason> is the reason as given by ISC dhclient and additionally"
    echo "		NONDHCP and NONDHCP6. Setting a <reason> is not meant to happen"
    echo "		in normal operation."
    echo
    echo
    echo "Let this script execute from cron, from ISC dhclient as an exit hook and/or from ifupdown."
    echo "Using cron with a short interval is recommended for dynamic, public IPs in a combination"
    echo "with other mechanisms."
    echo
    echo "For testing use:"
    echo "	ddnsupdate NONDHCP|NONDHCP6 <ipv4_address>|<ipv6_address>"
    echo "		-> register an address for the configured host"
    echo
    echo "	ddnsupdate DELETE|DELETE6"
    echo "		-> remove a registered address for the configured host"
    echo
    echo "	ddnsupdate"
    echo "		-> get ip and do register"
    echo
    exit 0
fi

# detect version switch
if [ "$(echo -n " $@ " | grep -c -E '^.* -v .*$|^.* --version .*$')" -gt 0 ]; then
    echo "$(basename $0) v$VERSION"
    echo -e "\t$VERSION_INFO"
    exit 0
fi

# detect silent switch (must preceede arguments)
if [ "$(echo -n " $@ " | grep -c -E '^.* -q .*$|^.* --quiet .*$')" -gt 0 ]; then
    DEBUG=false
    debug_flag="-q"
    shift
fi

# Check whether passed string is a valid fqdn
#
#	is_fqdn <string>
#		returns true or false
#
function is_fqdn() {
    
    local fqdn="$1"
    local host="$(echo "$fqdn" | sed -e "s/\..*$//")"

    # check host part
    if [ "$(is_host_name "$host")" = "false" ]; then
	echo "false"
	return 0
    fi
    
    # max length is 253 characters
    if [ ${#fqdn} -gt 253 ]; then
	echo "false"
    
    # compare length of given string with length of sanitized host name
    elif [ ${#fqdn} -eq $(echo "$fqdn" | tr -d '\n' | sed -e "s/[^a-zA-Z0-9.-]//" -e "s/-$//" | wc -m) ]; then
	echo "true"
    else
	echo "false"
    fi

    return 0
}

# Check whether passed string is a valid host name
#
#	is_host_name <string>
#		returns true or false
#
function is_host_name() {

    local host="$1"
    
    # max length is 253 characters
    if [ ${#host} -gt 63 ]; then
	echo "false"
    
    # compare length of given string with length of sanitized host name
    elif [ ${#host} -eq $(echo "$host" | tr -d '\n' | sed -e "s/[^a-zA-Z0-9-]//" -e "s/^-//" -e "s/-$//" | wc -m) ]; then
	echo "true"
    else
	echo "false"
    fi

    return 0
}

# check HOSTNAME
if [ -z "$HOSTNAME" ]; then
    echo "[ERROR] You need to supply a hostname. Edit the script and set the HOSTNAME variable in the script header." >&2
    exit 1
elif [ "$(is_fqdn "$HOSTNAME")" = "false" ]; then
    echo "[ERROR] The supplied hostname set in the script header contains illegal characters or is otherwise malformed." >&2
    exit 1
fi

# get the zone from hostname
ZONE="$(echo "$HOSTNAME" | sed -e "s/^[^.]\+\.//")"
if [ "$ZONE" = "$HOSTNAME" ]; then
    echo "[ERROR] The hostname set must include its domain name. Edit the script and set the HOSTNAME variable including the host's domain name in the script header." >&2
    exit 1
fi

# check SERVER
if [ -z "$SERVER" ]; then
    echo "[ERROR] You need to supply a name server to update. Edit the script and set the SERVER variable in the script header accordingly." >&2
    exit 1
fi

# Temporary file to hold the ip address
TMPFILE=/tmp/ddns_ip

# set PUBLIC_IP default
if [ "$PUBLIC_IP" != "false" ]; then
    PUBLIC_IP=true

    # check ip service
    if [[ -z "$MYIP_SERVICE" && ${#MYIP_SERVICES[@]} -eq 0 ]]; then
        echo "[ERROR] To determine the public IP an external service is needed. Please set the MYIP_SERVICE variable in the script header." >&2
        exit 1
    elif [ -n "$MYIP_SERVICE" ]; then
	# rewrite from old style variable name to array
	MYIP_SERVICES[0]="$MYIP_SERVICE"
	unset MYIP_SERVICE
    fi
    # check conformance of ip services names
    for ((i=0;i<${#MYIP_SERVICES[@]};i++)); do
	if [ "$(is_fqdn "$(echo "${MYIP_SERVICES[$i]}" | sed -e "s%^https\{0,1\}://%%" -e "s%\(:[0-9]\{1,5\}\)\{0,1\}/.*$%%" -e "s/^\[//" -e "s/\]\(:[0-9]\{1,5\}\)\{0,1\}$//")")" = "false" ]; then
	    echo "[ERROR] The IP service ${MYIP_SERVICES[$i]} set in the script header contains illegal characters or is otherwise malformed." >&2
	    do_exit=true
	fi
    done
    if [ -n "$do_exit" ]; then exit 1; fi

    # check fping
    if [ -z "$PING" ]; then
        PING="$(which fping)"
    fi
    if [ ! -x "$PING" ]; then
        echo "[ERROR] The program \"fping\" is needed for proper operation with public IPs. Please check." >&2
        exit 1
    fi
fi

# check key file
if [ -z "$KEYFILE" ]; then
    echo "[ERROR] A key file must be given. Please set the KEYFILE variable in the script header." >&2
    exit 1
elif [ ! -f "$KEYFILE" ]; then
    echo "[ERROR] The given key file \"$KEYFILE\" doesn't exist or is not a file. Please correct." >&2
    exit 1
fi

# declare UP_TEST_TARGET4/6

# QUAD9 public DNS
QUAD9_IP4="9.9.9.9"
QUAD9_IP6="2620:fe::fe"

# Host to ping to determine your connection state (only for public IPs needed).
UP_TEST_TARGET_IP4="$QUAD9_IP4"
UP_TEST_TARGET_IP6="$QUAD9_IP6"

# global array for ip to register
declare -a new_ip_address

# Obtain new IP address
obtain_ip() {

    local rv
    local exit_reason

    # make sure new_ip_address is empty
    new_ip_address=()

    # IP may be passed by DHCP, retrieved from the interface or from a public IP service

    if [ -z "$reason" ]; then
	# not called by DHCP

	if $PUBLIC_IP; then
	    # use a public ip service

	    # collect public address of all address families
	    for ipv in 6 4; do

		get_public_ip
		rv=$?

		# collect ip
		if [ -n "$new_ip_address" ]; then
		    new_ip_address[$ipv]="$new_ip_address"
		fi
	    done

	    # delete address on position 0
	    unset new_ip_address[0]

	    # check address collection
	    if [[ "${#new_ip_address[@]}" -eq 0 || $rv -eq 3 ]]; then

		# build final error message
		if [ $rv -eq 3 ]; then
		    exit_reason="Not connected to the internet"
		    # suppress all further errors
		else
		    exit_reason="Unable to determine public IP"
		    # print all error messages now (which has been done in debug mode already) and exit
		    if [ "$DEBUG" != "true" ]; then echo "$ERRMSG" | grep "^\[ERROR\]" >&2 ; fi
		fi

		echo "[ERROR] No DNS update will be performed. $exit_reason." >&2
		exit 1
	    fi

	else
	    # determine address of interface where default gateway is set
	    interface="$(ip route show default | sed -e "s/^default via [^ ]\+ dev \([^ ]\+\) .*$/\1/")"
	    if [ -n "$interface" ]; then
		new_ip_address="$(ip addr show "$interface" scope global | grep inet | sed -e "s/^.*inet6\{0,1\} \([^ ]\+\)\/[0-9]\+ .*$/\1/")"
	    fi
	    # no default route or no address on interface, so assume we're down
	    if [[ -z "$new_ip_address" || -z "$interface" ]]; then
		set_down_ip
	    fi
	fi

    elif [[ "$PUBLIC_IP" = "true"  && "$reason" != "NONDHCP" && "$reason" != "NONDHCP6" && "$reason" != "DELETE" && "$reason" != "DELETE6" ]]; then
        # obtain ip from an external service
        get_ip_family
        get_public_ip

        # check against expected IPs
	check_expected_ips

    elif [[ -n "$1" ]]; then
        # get IP from command line
        new_ip_address="$1"
        get_ip_family

        # check against expected IPs
	check_expected_ips
    fi
}

# determine IP version
get_ip_family() {
    if [ -n "$reason" ]; then
	# determine IP version
	if [ ${reason:$((${#reason}-1)):1} = "6" ]; then
		ipv=6
	else
		ipv=4
	fi
    else
	echo "[ERROR] Unable to determine IP address family, probably due to a coding error." >&2
	exit 255
    fi
}

# get public IP from myip service
get_public_ip() {

    local curl_insecure_switch=""


    # get_public_ip is called without an argument, it calls itself with the IP service to use
    if [ -z "$*" ]; then
	protocol_error=false

	# loop through each service in the array and call myself recursively
	for ((i=0;i<${#MYIP_SERVICES[@]};i++)); do
	    get_public_ip "${MYIP_SERVICES[$i]}"
	    ERR=$?

	    if [[ "$ERR" -eq 0 && -n "$new_ip_address" ]]; then
		# all good
		ERRMSG="$ERRMSG"$'\n'"[INFO] Public IPv$ipv address $new_ip_address received from $MYIP_SERVICE."
		break;
	    elif [ "$ERR" -eq 2 ]; then
		# remember protocol error
		protocol_error=true
	    elif [ "$ERR" -eq 3 ]; then
		# no connection
		set_down_ip
		reason="DOWN"
		# do not try any more ip services
		return 3
	    fi
	done

	# Output error messages collected so far without the first line which is empty
	$DEBUG && ERRMSG="$(echo "$ERRMSG" | tail -n +2)"
	$DEBUG && [ -n "$ERRMSG" ] && echo "$ERRMSG" && unset ERRMSG >&2
	if [ $i -eq ${#MYIP_SERVICES[@]} ]; then
	    # all services gave an error or warnings
	    if [[ "$ERR" -eq 1 && "$protocol_error" = "false" ]]; then
		$DEBUG && echo "[WARN] All IP service(s) failed for IPv$ipv requests." >&2
		return 1
	    fi
	fi
        return 0

    fi

    # get_public_ip was called with an argument, get it
    MYIP_SERVICE="$1"

    # extract host from ip service url
    MYIP_SERVICE_IP="$(echo "$MYIP_SERVICE" | sed -e "s%^https\{0,1\}://%%" -e "s%\(:[0-9]\{1,5\}\)\{0,1\}/.*$%%" -e "s/^\[//" -e "s/\]\(:[0-9]\{1,5\}\)\{0,1\}$//")"

    # make sure to connect with the right protocol family if a numerical ip address was given for the myip service
    if [[ "$(is_ipv6_address "$MYIP_SERVICE_IP")" = "true" && $ipv -eq 4 ]]; then
	# a numerical IPv6 address for the myip service was given trying to get an IPv4 public address -> nothing to do
	unset new_ip_address[0]
	ERRMSG="$ERRMSG"$'\n'"[INFO] Can't get public IPv4 address using an IP service addressed with a numerical IPv6 address ($MYIP_SERVICE_IP)."
	return 0
    elif [[ "$(is_ipv4_address "$MYIP_SERVICE_IP")" = "true" && $ipv -eq 6 ]]; then
	# a numerical IPv4 address for the myip service was given trying to get an IPv6 public address -> nothing to do
	unset new_ip_address[0]
	ERRMSG="$ERRMSG"$'\n'"[INFO] Can't get public IPv6 address using an IP service addressed with a numerical IPv4 address ($MYIP_SERVICE_IP)."
	return 0
    fi

    # allow invalid certificates?
    if [ "$MYIP_SERVICE_ALLOW_INSECURE_TLS" = "true" ]; then
	curl_insecure_switch="--insecure"
    fi

    new_ip_address="$(curl -$ipv --silent $curl_insecure_switch --max-time 10 "$MYIP_SERVICE" 2>&1)"
    ERR=$?

    # treat curl result
    if [ $ERR -ne 0 ]; then

        # are we online? (Use indirection with UP_TEST_TARGET)
	UP_TEST_TARGET="UP_TEST_TARGET_IP$ipv"
	ping_target "${!UP_TEST_TARGET}"
	putt_rv=$?

        if [[ "$PING_ANSWER" = "$ALIVE_ANSWER" && ( $ERR -eq 7 || $ERR -eq 6 || $ERR -eq 28 || $ERR -eq 60 || $ERR -eq 3) ]]; then
	    # we're online but ip service has a network error:
	    # either IP service is unreachable or we used the wrong ip version

	    # Certificate invalid
	    if [ $ERR = 60 ]; then
		ERRMSG="$ERRMSG"$'\n'"[ERROR] TLS certificate of $MYIP_SERVICE_IP invalid."
		return 1
	    elif [ $ERR = 3 ]; then
		ERRMSG="$ERRMSG"$'\n'"[ERROR] Malformed URL: $MYIP_SERVICE"
		return 1
	    fi

	    # ping IP service
	    ping_target "$MYIP_SERVICE_IP"
	    pmis_rv=$?
            if [ "$PING_ANSWER" = "$ALIVE_ANSWER" ]; then
		# wrong protocol used
		unset new_ip_address[0]
		ERRMSG="$ERRMSG"$'\n'"[INFO] IP service $MYIP_SERVICE_IP does not answer to my IPv$ipv requests."
		return 2
	    elif [ $pmis_rv -eq 1 ]; then
		ERRMSG="$ERRMSG"$'\n'"[WARN] IP service $MYIP_SERVICE_IP does not answer to pings or gave a network error."
		return 1
	    elif [ $pmis_rv -eq 2 ]; then
		if [ "$(has_dns_entry "$MYIP_SERVICE_IP")" = "false" ]; then
		    ERRMSG="$ERRMSG"$'\n'"[INFO] IP Service $MYIP_SERVICE_IP has no IPv$ipv address."
		else
		    ERRMSG="$ERRMSG"$'\n'"[ERROR] DNS lookup of IP Service $MYIP_SERVICE_IP failed in IPv$ipv address space."
		fi
		return 1
	    else
		ERRMSG="$ERRMSG"$'\n'"[ERROR] IP Service $MYIP_SERVICE_IP failed."
		return 1
	    fi

	elif [ $putt_rv -gt 0 ]; then
            # we're offline in the current universe
            ERRMSG=$'\n'"[INFO] No IPv$ipv connection to the internet."
	    return 3
        else
            # don't change IP, we just don't know without our ip service
            if [ ! -z "$new_ip_address" ]; then
                ERRMSG="$ERRMSG"$'\n'"[ERROR] IP Service $MYIP_SERVICE_IP failed: $new_ip_address."
            else
                ERRMSG="$ERRMSG"$'\n'"[ERROR] IP Service $MYIP_SERVICE_IP failed (curl error $ERR)."
            fi
            return 1
        fi

    elif [ -z "$new_ip_address" ]; then
        # connection is working, service did return an empty string
        ERRMSG="$ERRMSG"$'\n'"[ERROR] IP Service $MYIP_SERVICE_IP was reachable, but returned an empty string."
        return 1
    else

	# no error from curl: extract IP from checkip service

	# Is the returned string an ip address?
	if [ "$(is_ip_address "$new_ip_address")" = "false" ]; then

	    # try in checkip format
	    new_ip_address="$(echo "$new_ip_address" | html2text | grep -m 1 "Current IP Address: " | sed -e "s/^Current IP Address: //g")"
	    # check again
	    if [ "$(is_ip_address "$new_ip_address")" = "true" ]; then
		return 0
	    fi

	    # no usable ip received
	    ERRMSG="$ERRMSG"$'\n'"[ERROR] The IP Service under $MYIP_SERVICE_IP seems not to return an IPv$ipv address."
	    unset new_ip_address[0]
	    return 1
	fi

    fi
    return 0
}

# Check whether passed host has a dns entry
#
#	has_dns_name <string> [4|6]
#		returns true or false
#
function has_dns_entry() {

    local queryhost="$1"
    local ip_family="$2"
    local recordtype
    local rv
    local dig_result
    
    # set ip family to global value, if passed arg isn't valid or missing
    if [[ "$2" != "4" && "$2" != "6" ]]; then
	ip_family="$ipv"
    fi
    
    # determine record type
    if [ $ip_family -eq 6 ]; then
            recordtype="AAAA"
    else
            recordtype="A"
    fi
    
    # query host
    dig_result="$(dig +short "$1" "$recordtype")"
    rv=$?
   
   # process answer
   if [ $rv -eq 0 ]; then
       if [ -z "$dig_result" ]; then
	   # no DNS entry found in respective address space
	   echo "false"
       else
	   # DNS entry found
	   echo "true"
       fi
       return 0
   fi

   # dig failed
   echo "$dig_result"
   return $rv
   
}

# Check whether passed string is an ip address
#
#	is_ip_address <string> [4|6]
#		returns true or false
#
function is_ip_address() {

    local ip_family="$2"

    # set ip family to global value, if passed arg isn't valid or missing
    if [[ "$2" != "4" && "$2" != "6" ]]; then
	ip_family="$ipv"
    fi

    case $ip_family in

       4)
	    is_ipv4_address "$1"
       	    ;;

       6)
	    is_ipv6_address "$1"
	    ;;

       *)
	    # ip version not set
	    echo "BUG: please set ip version in var ipv" >&2
	    return 1
    esac
}

# Check whether passed string is an IPv4 address
#
#	is_ipv4_address <string>
#		returns true or false
#
function is_ipv4_address() {

    local ip

    # truncate to 15 bytes (4 times 3 chars, plus separators) of first line
    ip="$(echo "$1" | head -1 | dd bs=1 count=15 2>/dev/null)"

    # check format
    if [ $(echo "$ip" | grep -c '^\([0-9]\{1,3\}\.\)\{3,\}[0-9]\{1,3\}$') -eq 1 ]; then
	echo "true"
    else
	echo "false"
    fi

    return 0
}

# Check whether passed string is an IPv6 address
#
#	is_ipv6_address <string>
#		returns true or false
#
function is_ipv6_address() {

    local ip

    # truncate to 39 bytes (8 times 4 chars, plus separators) of first line
    ip="$(echo "$1" | head -1 | dd bs=1 count=39 2>/dev/null)"

    # check format
    if [[ $(echo "$ip" | grep -c ":") -ge 1 && $(echo "$ip" | sed -e "s/^[0-9a-f:]\{1,\}$//") = "" ]]; then
	echo "true"
    else
	echo "false"
    fi

    return 0
}

# Check IP against list of expected IPs
check_expected_ips() {

    if [[ -n "$new_ip_address" && -n "$EXPECTED_IPS" ]]; then

        # convert expected ips to all numerical
        for ip in $EXPECTED_IPS; do
            if [ $(echo "$ip" | grep -cE '^[0-9.:a-f]+$') -eq 0 ]; then
                # resolve
                ip="$(dig $ip +short)"
                [ -z "$ip" ] && continue;
            fi
            expected_ips="$expected_ips $ip"
        done
        EXPECTED_IPS="${expected_ips:1}"

        # check against expected ip
        found=false
        for ip in $EXPECTED_IPS; do
            if [ "$new_ip_address" = "$ip" ]; then
                    found=true
                    break;
            fi
        done
        if ! $found; then
            # not an expected IP go to fallback

	    # for backward compatibility check old variable aswell
	    local oldfallbackip="$FALLBACKIP"
	    local fallbackip4or6="FALLBACKIP$ipv"
	    local FALLBACKIP="${!fallbackip4or6}"
	    if [ -z "$FALLBACKIP" ]; then FALLBACKIP="$oldfallbackip"; fi

            # set fallback ip
            if [ -n "$FALLBACKIP" ]; then
                $DEBUG && echo "Unexpected IP found ($new_ip_address). Using fallback: $FALLBACKIP" >&2
                new_ip_address="$FALLBACKIP"
            else
                $DEBUG && echo "Unexpected IP found ($new_ip_address). No fallback given, considering host as down." >&2
                set_down_ip
                reason="DOWN"
            fi
        fi
    fi
}

# ping a target
ping_target() {
    PING_ANSWER="$($PING -$ipv -B 2 -r $RETRIES -p 50 "$1" 2>&1)"
    PING_ERRNUM=$?
    if [ $PING_ERRNUM -eq 0 ]; then
	PING_ANSWER="$ALIVE_ANSWER"
    fi
    return $PING_ERRNUM
}

# set ip to unassigned or DOWNIP
set_down_ip() {

    # for backward compatibility check old variable aswell
    local olddownip="$DOWNIP"
    local downip4or6="DOWNIP$ipv"
    local DOWNIP="${!downip4or6}"

    if [[ -z "$DOWNIP" && -z $olddownip ]]; then
	new_ip_address="unassigned"
    elif [ ! -z "$DOWNIP" ]; then
        new_ip_address="$DOWNIP"
    elif [ ! -z "$olddownip" ]; then
        new_ip_address="$olddownip"
    fi
}

# read previous IP from disk
get_previous_ip() {
    if [ -e "${TMPFILE}${ipv}" ]; then
	OLDIP="$(< "${TMPFILE}${ipv}")"
    else
        OLDIP=unassigned
    fi
}

# delete state file
forget_previous_ip() {

    if [ -e "${TMPFILE}${ipv}" ]; then
        rm "${TMPFILE}${ipv}" 2>/dev/null
    fi
}

# check the currently registered IP in DNS
get_registered_ip() {
    local regip
    local rv

    # determine record type
    if [ $ipv -eq 6 ]; then
            recordtype="AAAA"
    else
            recordtype="A"
    fi

    # get currently registered IP from our DNS we update
    regip="$(dig $recordtype +noall +short +timeout=5 "$HOSTNAME" @"$SERVER" 2>/dev/null)"
    rv=$?
    if [[ $rv -eq 9 && -z "$regip" ]]; then
	# get from our resolver since there was no answer
	regip="$(dig $recordtype +noall +short +timeout=5 "$HOSTNAME" 2>/dev/null)"
    fi

    # write to registration file or delete it
    if [ -n "$regip" ]; then
	echo "$regip" > "${TMPFILE}${ipv}"
    elif [ $rv -eq 0 ]; then
	rm "${TMPFILE}${ipv}" 2>/dev/null
    fi
}

# recheck registered address
recheck_registered_address() {
    # check registration file existence and whether recheck is enabled
    if [[ -f "${TMPFILE}${ipv}" && -n "$FORCE_RECHECK_MINUTES" ]]; then
	# calculate grace period
	if [ $(( $(date +%s) - $(stat -c %Y "${TMPFILE}${ipv}") )) -ge $((FORCE_RECHECK_MINUTES*60)) ]; then
	    get_registered_ip
	fi
    elif [ ! -f "${TMPFILE}${ipv}" ]; then
	get_registered_ip
    fi
}

# return update command
construct_update_cmd() {

    local recordtype
    local update_data
    local update_data_prepared
    local oldip

    # determine record type
    if [ $ipv -eq 6 ]; then
            recordtype="AAAA"
    else
            recordtype="A"
    fi

    # prepare data
    update_data_prepared="${HOSTNAME}. $TTL IN $recordtype"

    case "$1" in

        add|update)
            if [ "$OLDIP" != "unassigned" ]; then
		while read -r oldip; do
                    update_data="${update_data}$(construct_update_cmd delete)"$'\n'
		done <<< $OLDIP
            fi
            echo "${update_data}update add $update_data_prepared $new_ip_address"
            ;;

        delete)
            echo "update delete $update_data_prepared"
            ;;
    esac
}

# notify DNS if we have a new ip
ddns_ip_do() {
    local update_cmd

    # read previous IP
    get_previous_ip

    # prepare command
    update_cmd="$(construct_update_cmd "$1")"

    case "$1" in

        add|update)
            # determine whether ip has changed and
            if [ "$new_ip_address" != "$OLDIP" ]; then
                # ip changed most probably
                update_dns "$update_cmd" "$HOSTNAME $new_ip_address"
                rv=$?
                if [ $rv -eq 0 ]; then
                        $DEBUG && echo "   $(echo "$OLDIP" | tr '\n' ' ')--> $new_ip_address"
                        echo $new_ip_address > "${TMPFILE}${ipv}"
                else
                        echo -e "[ERROR] Failed to update hostname in DNS: \n\t$ERR"
                        forget_previous_ip
                fi
            else
                $DEBUG && echo "[INFO] No IPv$ipv address change detected." >&2
            fi
            ;;

        delete)
	    # check whether there is still an IP registered in DNS
	    if [ "$new_ip_address" != "$OLDIP" ]; then
		update_dns "$update_cmd" "Deleting IPv$ipv record $OLDIP"
		rv=$?
		if [ $rv -eq 0 ]; then
		    forget_previous_ip
		fi
	    fi
            ;;
    esac
    return $rv
}

# execute nsupdate update function
update_dns() {
        $DEBUG && echo -n "Updating DNS on $SERVER ($reason): $2..." >&2
        ERR="$(nsupdate -v -k "$KEYFILE" 2>&1 << EOF
	    server $SERVER
	    $1
	    send
EOF
        )"

        if [ $? -ne 0 ]; then
                $DEBUG && echo "  failed: $ERR" >&2
                return 1
        else
                $DEBUG && echo " ok." >&2
                return 0
        fi
}

# get reason from dhclient variable
reason="$1"

# act on dhcp reason
case "$reason" in

    "")

	# called by cron or manually (no reason given)
	# obtain ip, determine ip version, set reason and call myself

	# determine debug mode of bash
	if [ $(echo "$-" | grep -c "x") -eq 1 ]; then
	    bash_debug="-x"
	else
	    bash_debug="+x"
	fi

	# obtain ip (returns an array new_ip_address: index 4 -> IPv4, index 6 -> IPv6)
	obtain_ip

	# treat each address family and call myself
	for ipv in 6 4; do

	    # DHCP reasons have no postfix with IPv4 and "6" with IPv6
	    if [ "$ipv" -eq 6 ]; then
		dhcp_postfix="$ipv"
	    else
		unset dhcp_postfix
	    fi

	    # obtain_ip should not give back an empty array
	    if [ -z "${new_ip_address[$ipv]}" ]; then
		if [ "$DEBUG" = "false" ]; then
		    # print all error messages up to now (in debug mode they have alreaady been printed)
		    echo "$ERRMSG" | grep "^\[ERROR\]" >&2
		    # ...and forget them
		    unset ERRMSG
		fi
		echo "[ERROR] No IPv$ipv address obtained." >&2
		continue;

	    # delete the entry with no assigned address
	    elif [ "${new_ip_address[$ipv]}" = "unassigned" ]; then
		bash "$bash_debug" "$0" $debug_flag "DELETE$dhcp_postfix"

	    # call myself with a single address for registration
	    else
		bash "$bash_debug" "$0" $debug_flag "NONDHCP$dhcp_postfix" "${new_ip_address[$ipv]}"
	    fi

	done
	rv=0
	;;


    BOUND|RENEW|REBIND|REBOOT|BOUND6|RENEW6|REBIND6|REBOOT6|NONDHCP|NONDHCP6)

	# determine IP to update
	if [ -z "$2" ]; then
	    echo "[ERROR] An IP address as a second argument to reason \"$reason\" is mandatory." >&2
	    exit 1
	fi

	# get IP of host
	obtain_ip "$2"

	# check registration
	recheck_registered_address

	# update ip if needed
        ddns_ip_do update
        rv=$?
        ;;


    EXPIRE|RELEASE|STOP|EXPIRE6|RELEASE6|STOP6|DELETE|DELETE6)
        # notify DNS we're down

        get_ip_family
        set_down_ip
        get_registered_ip
        ddns_ip_do delete
        rv=$?
        ;;

    PREINIT|TIMEOUT|PREINIT6|TIMEOUT6|FAIL|DOWN)
        # ignore
	rv=0
        ;;

    *)
        # Unknown DHCP reason
        echo "$(basename "$0"): DHCP reason $reason unknown. Nothing done." >&2
        rv=1
        ;;
esac

exit $rv


# Hook choice -> Problem: enter hook RELEASE acts after connection is down!
is_hook() {
    if [[ -n "$hook" && "dhclient-$1-hook" != "$hook" ]]; then
        exit 0
    fi
}
        # run as enter hook only
        hook="$3"
        is_hook enter
