#! /bin/bash

# We run on a vassal machine.
# We find a globally-valid IPv6 address and
# update the relevant DNS servers accordingly.

declare -A servers

function usage() {
<<EoF cat
Script to update DNS records according to what
IP addresses this host appears to be using.

Usage:
   $0 [options]

Options include:
  -H hostname   # A hostname or alias (FQDN) for this system.
                # Use multiple -H options for multiple aliases.
                # These hostnames will have their DNS records updated.
                # If no -H options are specified, we use the contents
                # of /etc/hostnames (plural), or failing that, the
                # plain old \$(hostname).
  -v            # Print verbose information about what we think we are doing
  -s 1.2.3.4    # Add something to list of aux_dns_servers
  -s r          # Read /etc/resolv.conf and add to aux_dns_servers
  -n            # no-op (don't actually change anything)
  -a            # Ascertain WAN IP addresses _of the gateway_.
                # Ascertain it empirically, by asking a remote server.
                # In contrast, the default is to use the IP addresses
                # of this host, based on what is in the routing tables,
                # put there presumably by ./6to4 (on a gateway)
                # or by router discovery (on a vassal).
                # Typical usage:
                # update-ns -6 # because ipv6 is passed through (not fowarded).
                # update-ns -4 -a # because ipv4 is fowarded (i.e. natted).
                # $0 -a -n -v is harmless and possibly informative.
                # $0 -a -4 makes sense on a vassal, if the gateway
                #   is port-forwarding some service(s) to you.
                # $0 -6 -a does not make much sense on a vassal.
  -W 1.2.3.4    # The host's WAN address (IPv6 if colons, IPv4 otherwise)
                # Good for testing, to force an update of the records.
                # Maybe (?) useful if you are offline and know the IP.
                # Default is to determine the address empirically,
                # based on what "ip route get" says.
  -4            #* Update the DNS A records.
  -6            #* Update the DNS AAAA records.
  -other TXT "a b c"  #* Set the TXT record
  -x4           #* Delete the DNS A records.
  -x6           #* Delete the DNS AAAA records.
  -xother TXT   #* Delete the DNS TXT records.
  -h            # Print this message and exit.

If you don't specify any of the four starred "action" options,
the default is to do just -6.

Note: If this host appears to be have the same name
*or* the same address as one of the DNS servers, we
(FIXME -- not implemented)
attempt to contact the DNS server via its "local"
address.  This is necessary for two reasons:
 1) If the IP address needs to be updated, we can't do it by
    contacting the server at the out-of-date address.
 2) If we are behind a NAT box, we can't contact the server
    by uttering the NAT box's WAN-side address.

The variable "active_dns" holds a list of addresses of any special DNS
servers you want updated (in addition to the non-special ones found
via the host's NS records).  This can be a lifesaver in a dynamic_IP
situation.  You might think it doesn't make sense to have a DNS server
with a dynamic IP address, because if the address changes the outside
world loses contact with it, and fixing the problem from outside would
be a chicken-and-egg problem.  However, active_dns (which may be the
same as localhost, or the NAT gateway) may allow us to fix the problem
/from the inside/.

If aux_dns_servers="r" then we obtain server addresses by reading
/etc/resolv.conf.

To repeat: special server addresses are in addition to (not instead
of) whatever non-special servers are listed in each host's NS records.

We assume you have a collection of DNS keys in /etc/lib/dyndns.d/
and your DNS servers are configured to accept those keys.  A
keyfile with three underscores of the form  K___.foo.com.*.key
(if it exists) is assumed to have power over the entire foo.com
subnet.  Other keyfiles are assumed to have power over the
correspondingly named individual host.

EoF
  return
}

################
# Settings:

: ${ip_addr_oracle:=www.ipaddressworld.com}
keydir=/etc/lib/dyndns.d
vtime=3600      # desired duration (in seconds) of validity of DNS records

# end settings.
################

# Load some functions that do most of the work:
bindir=$( dirname $0 )
if test -z "$have_ip_conf_utils" ; then
  . $bindir/ip-conf-utils
fi

while test -n "$*" ; do
  opt=$1 ; shift
  case $opt in
    -help|-h)
      usage
      exit 0
      ;;
    -H)
      arg=$1; shift
      hostlist="$hostlist $arg"
      ;;
    -W)
      arg=$1 ; shift
      if test "_${arg}" != "_${arg#*:}" ; then
        ip6addr=$arg
      else
        WANip4=$arg
      fi
      ;;
    -mac|-MAC)
      arg=$1 ; shift
      maclist="$maclist $arg"
      ;;
    -a)
      mode_a=yes
      ;;
    -s)
      arg=$1 ; shift
# The "aux_dns_servers" address is sometimes a lifesaver in a
# dynamic-IP situation ... as described in the help message.
      if test "_$arg" = "_r" ; then
        aux_dns_servers="$aux_dns_servers $( get_resolv_conf_nameservers )"
      else
        aux_dns_servers="$aux_dns_servers $arg"
      fi
      1>&2 echo "aux_dns_servers is now '$aux_dns_servers'"
      ;;
    -n)
      mode_n=yes
      ;;
    -v)
      mode_v=yes
      ;;
    -t)
      mode_t=yes
      ;;
    -other)
      mode_other=yes
      rrtype_other=$1 ; shift
      body_other=$1 ; shift
      ;;
    -4)
      mode_4=yes
      ;;
    -6)
      mode_6=yes
      ;;
    -x4)
      mode_4=yes
      x4=';'
      ;;
    -x6)
      mode_6=yes
      x6=';'
      ;;
    -xother)
      mode_other=yes
      rrtype_other="$1" ; shift
      xother=';'
      ;;
    *)
      1>&2 echo "Extraneous verbiage '$opt'"
      exit 1
  esac
done

if test -z "$mode_4" -a -z "$mode_6" -a -z "$mode_other"; then
  mode_6=yes
fi

# Get servers relevant to a particular host.
# Usage: get_servers "host.sub.net"
# Side effect on variable ${servers[$host]}
function get_servers() {
    local host=$1
    local subnet="--x--"
    servers[$host]=''
    msg=$( dig +short $host ns ) && servers[$host]="$msg"   \
      || 1>&2 echo "get_servers: monkey business"
    if test -z "${servers[$host]}" ; then
      subnet=${host#*.}
      msg=$( dig +short $subnet ns ) && servers[$host]="$msg"
    fi
    if test -z "${servers[$host]}" ; then
      1>&2 echo "No DNS servers for $host or $subnet;"
      1>&2 echo "No DNS records for $host will be updated."
    fi
}

### Calculate a few useful names and addresses ###

: ${hostlist:=$( 2>/dev/null cat /etc/hostnames )} || true
: ${hostlist:=$( hostname -f )}
new_WANip4=''
if test -z "$WANip4" -o -z "$ip6addr" ; then
  if test -n "$mode_a" ; then
    # ascertain IP addr empirically :
    < <(fancy_get_WAN_addr) MapFile new_WANip4 new_ip6addr
  else
    # extract info from local routing tables :
    < <( get_our_ipaddrs ) MapFile new_ip6addr new_WANip4 odev6_not_used_here
    if test -z "$new_WANip4" ; then
      1>&2 echo "Failed to get IPv6 and IPv4 addresses from routing tables."
      exit 2;
    fi
    ## 1>&2 echo ">>>>>>> new_ip6addr '$new_ip6addr'  new_WANip4 '$new_WANip4'"
  fi
fi
# be smart about the case where one of them needs a
# default value but the other does not:
: ${WANip4:=$new_WANip4} ${ip6addr:=$new_ip6addr}

if test -n "$mode_v" ; then
  printf "%4s:  %-40s (to_be_checked: %s)\n" A    "$WANip4"  "${mode_4:-no}"
  printf "%4s:  %-40s (to_be_checked: %s)\n" AAAA "$ip6addr" "${mode_6:-no}"
  if test -n "$mode_other" ; then
    printf "%4s:  %-40s (to_be_checked: %s)\n" "$rrtype_other" \
         "$body_other" "${mode_other:-no}"
  fi
  printf "aux_dns_servers: %s\n" "$aux_dns_servers"
  ##printf "hosts:     %s\n" "$hostlist"
  printf "hosts and servers:\n";
  for host in $hostlist ; do
    echo "   $host"
    get_servers $host
    set -- ${servers[$host]}
    while test -n "$*" ; do
      server="$1" ; shift
      if test -n "$*" ; then
        echo "    |--> $server"
      else
        echo "    \\--> $server"
      fi
    done
  done
fi

if test -n "$mode_n" ; then exit 0 ; fi

##////////////////////////

if test -n "$mode_4" -o -n "$mode_6" -o -n "$mode_other"; then
   #update_cmd=/etc/network/justcat     ## for testing
   #update_cmd=/bin/echo                ## for testing
   update_cmd=nsupdate

## Preliminary outer loop over all hostnames of this host,
## to see if this hostname matches a second-level nameserver name.
  for myhost in $hostlist ; do
    host="$myhost"
    get_servers "$host"

    subnetlevel=$( echo -n $subnet | sed 's/[^.]//g' | wc -c )  ## number of dots

## Loop over all servers,
## to see if this hostname matches a second-level nameserver name.
    specialhost=""
    for server in ${servers[$host]} ; do
      server=${server%.}                ## strip ugly trailing .
      echo toplevel testing "_$host" versus "_$server" level $subnetlevel
##????      if test "_$host" = "_$server" -a $subnetlevel -eq 1 ; then
      if test "_$host" = "_$server" ; then
        ## Bingo! Got a second-level nameserver
        specialhost=yes
        echo "Host '$host' appears to be a nameserver."
        tld=${subnet#*.}
        tldserver=$( dig +short $tld ns | head -1 )

        if false ; then
          echo "... host: ($hostlevel) $host  subnet: ($subnetlevel) $subnet"
          echo "... tld: $tld  tldserver: $tldserver"
        fi

        tldserver=${tldserver%.}                ## strip ugly trailing .
        problem=""

        tld_op_h_ip=''
        msg=$( tld_dig $tldserver $host ) && tld_op_h_ip="$msg" || true
        : ${tld_op_h_ip:='(nil)'}
        if test -n "$redo" -o "_$tld_op_h_ip" != "_$WANip4" ; then
          1>&2 echo "**** You need to notify the top-level servers via your domain registrar."
          1>&2 echo "**** Update '$host' from $tld_op_h_ip to $WANip4 asap."
          problem=yes
        fi

# check for self-consistency; get host's opinion of its own IP:
        h_op_h_ip=''
        msg=$( dig +short @$WANip4 $host ) && h_op_h_ip="$msg" || true
        : ${h_op_h_ip:='(nil)'}
        #--- h_op_h_ip=XXXX  # for testing
        if test -n "$redo" -o "_$h_op_h_ip" != "_$WANip4" ; then
          1>&2 echo "** Host '$host' is at $WANip4 according to $tldserver,"
          1>&2 echo "**   but it resolves itself to $h_op_h_ip"
          problem=yes
          servers[$host]="$servers[$host]
$tld_op_h_ip"
        fi

        if test -z "$problem" ; then
          1>&2 echo "Host '$host' is OK at $tldserver and is self-consistent."
        fi
      fi
    done # checking all servers that might be second-level nameservers
  done # checking all hosts


## Main outer loop over all hostnames of this host.
## This time actually do the updating.
  for host in $hostlist ; do
    if test -n "$mode_v" ; then
      1>&2 echo "update-ns: main outer loop: handling hostname '$host' +++++++"
    fi

## Main inner loop over all servers.
## This time actually do the updating.
    did_check_server="/"
    for server in ${servers[$host]} $aux_dns_servers ; do
      if test -n "$mode_v" ; then
        1>&2 echo "update-ns:      inner loop: checking $host at server '$server'"
      fi
      tmp=$(tempfile)
      # Conduct a little race and see who wins:
      while true ; do
        wait -n
        if (test $? = 127) then break ; fi
      done
      2>/dev/null ping6 -c 1 -n -W 3 $server >> $tmp &
      2>/dev/null ping  -c 1 -n -W 3 $server >> $tmp &
      while true ; do
        wait -n
        rslt=$?
# wait for any subjob to succeed, or all subjobs to finish,
# whichever comes first:
        if (test $rslt = 127 -o $rslt = 0) then break ; fi
      done
      server_ip=$(
        while read -r x1 x2 x3 x4 junk ; do
          if test "_$x1" = '_PING' ; then
            tried="$x2$x3"
            tried=${tried##*(}
            tried=${tried%%)*}
            ##xx 1>&2 echo "just tried $tried ........"
          fi
          if test "_$x2$x3" = "_bytesfrom" ; then
            ip="${x4%:}"      # strip silly trailing ":" if any
            echo "$ip"
            break
          fi
        done < $tmp
      )
      if test -z "$server_ip" ; then
        1>&2 echo "??? Could not ping server '$server'"
        if test -n "$mode_v" ; then
          1>&2 cat $tmp
          1>&2 echo "^^^^^^^^^^^^^^^^^"
        fi
      fi
      rm $tmp

      for block in once ; do
# Use for-block plus break statements to avoid lots of
# nested if/else/fi blocks.
        if test -z "$server_ip" ; then
          break # error message already printed
        fi

        if test "_${did_check_server%/$server_ip/}" = "_${did_check_server}" ; then
          if test -n "$mode_v" ; then
            1>&2 echo "Server $server @ '$server_ip' will now be checked."
          fi
        else
          if test -n "$mode_v" ; then
            1>&2 echo "Server $server @ '$server_ip' has already been checked."
          fi
          break
        fi

# checked means "check started" ... not necessarily completed successfully
        did_check_server="$did_check_server$server_ip/"
        ##?? 1>&2 echo "   did_check_server: $did_check_server"

        do_4=$mode_4
        do_6=$mode_6
        do_other=$mode_other

        if test -z "$WANip4" ; then
          x4=';'
        fi
        if test -z "$ip6addr" ; then
          x6=';'
        fi
        if test -z "$body_other" ; then
          xother=';'
        fi

        if test -n "$x4" ; then
          WANip4=""
        fi

        if test -n "$x6" ; then
          ip6addr=""
        fi

        pretty_WANip4=$WANip4
        : ${pretty_WANip4:=(nil)}
        pretty_ip6addr=$ip6addr
        : ${pretty_ip6addr:=(nil)}

        if test -n "$do_4"; then
          old4=$( dig @$server_ip +short $host a )
          if test "_$old4" = "_$WANip4" ; then
             echo "DNS A record for $host == $pretty_WANip4 is OK" \
                "on server $server at $server_ip"
             do_4=""
          fi
        fi

        if test -n "$do_6"; then
          old6=$( dig @$server_ip +short $host aaaa )
          if test "_$old6" = "_$ip6addr" ; then
             echo "DNS AAAA record for $host == $pretty_ip6addr is OK" \
                "on server $server at $server_ip"
             do_6=""
          fi
        fi

## maybe should have a stanza here to check for TXT record OK

        if test -n "$do_4" -o -n "$do_6" -o -n "$do_other"; then
          keyfile=$( 2>/dev/null ls -1 $keydir/K${host}.+???+*.key | head -1 )
          if test -z "$keyfile" ; then
            keyfile=$( 2>/dev/null ls -1 $keydir/${host}.tsig | head -1 )
          fi
          if test -z "$keyfile" ; then
            subdomain=${host#*.}
            keyfile=$( 2>/dev/null ls -1 $keydir/K___.$subdomain.+???+*.key \
                        | head -1 )
            if test -z "$keyfile" ; then
              1>&2 echo "No     keyfile: $keydir/K$host.+???+*.key"
              1>&2 echo "And no keyfile: $keydir/K___.$subdomain.+???+*.key"
              1>&2 echo "DNS records for '$host' will not be updated."
              break
            fi
          fi
        fi
        #??? 1>&2 echo "========= Using keyfile $keyfile"

# Update IPv4 address:
        if test -n "$do_4" ; then
          echo Updating A record for host $host to $pretty_WANip4"\
            "on server $server at $server_ip.
          if ! <<EoF $update_cmd -v -k $keyfile
            server $server_ip 53
            update delete $host.   1234   IN   A
            $x4 update add    $host.   $vtime IN   A $WANip4
            send
EoF
          then
            1>&2 echo Update failed for $host on server $server at $server_ip.
          fi
        fi  ## do_4

# Update IPv6 address:
        if test -n "$do_6" ; then
          echo Updating AAAA record for host $host to $pretty_ip6addr" \
          "on server $server at $server_ip.
          if ! <<EoF $update_cmd -v -k $keyfile
            server $server_ip 53
            update delete $host.   1234   IN   AAAA
            $x6 update add    $host.   $vtime IN   AAAA $ip6addr
            send
EoF
          then
            1>&2 echo Update failed for $host on server $server at $server_ip.
          fi
        fi  ## do_6

        if test -n "$do_other" ; then
          echo "Updating $rrtype_other record for host $host to \"$body_other"\" \
            "on server $server at $server_ip."
          if ! <<EoF $update_cmd -v -k $keyfile
            server $server_ip 53
            update delete $host.   1234   IN   $rrtype_other
            $xother update add    $host.   $vtime IN   $rrtype_other "$body_other"
            send
EoF
          then
            1>&2 echo "Update ($rrtype_other) failed" \
                "for $host on server $server at $server_ip."
          fi
        fi  ## do_other


      done      # block do once
    done        # loop over servers
  done          # loop over hosts in hostlist
fi
