#!/usr/bin/bash
# -*- mode: sh; sh-basic-offset: 2; indent-tabs-mode: nil; -*-
# vim:set ft=sh et sw=2 ts=2:
#
# 09-ddns v1.3.0 - NetworkManager dispatch for ipv4 Dynamic DNS updates
# Author: Scott Shambarger <devel@shambarger.net>
#
# Copyright (C) 2014-2021 Scott Shambarger
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Instructions for use:
#
#   Put this script in /usr/lib/NetworkManager/dispatcher.d (or wherever
#   your distro has these files)
#
#   The settings are discussed in NMUTILS/ddns-functions.
#
#   NOTE: By default, A and AAAA records use the global addresses
#   on an interface (see DDNS_RREC_<rrec>_PRIVATE to also consider
#   private addresses), so only set DDNS_RREC_A_VALUE or
#   DDNS_RREC_AAAA_VALUE if you wish to overrides those values
#   with static ones.
#
# Requires:
#
#   NMUTILS/ddns-functions - dynamic DNS functions
#   NMUTILS/general-functions - shared functions
#
# Config location:
#
#   NMCONF/ddns-<interface>.conf (see $NMUTILS/ddns-functions for settings)
#
# State file:
#
#   DDNS_STATE_DIR/ddns-<interface>-<rrec>.state
#
# shellcheck disable=SC1090,SC2153

# for logging
NMG_TAG=${NMG_TAG:-nmddns}

# set NMUTILS/NMCONF early, and allow environment to override
NMUTILS=${NMUTILS:-/etc/nmutils}
NMCONF=${NMCONF:-${NMUTILS}/conf}

########## SCRIPT START

# shellcheck disable=SC2034
NMDDNS_REQUIRED="1.5.0"

# load ddns-functions
NMDDNS=${NMDDNS:-${NMUTILS}/ddns-functions}
{ [[ -r ${NMDDNS} ]] && . "${NMDDNS}"; } || {
  echo 1>&2 "Unable to load ${NMDDNS}" && exit 2; }

[[ ${NMDDNS_VERSION} ]] || {
  nmg_err "${0##*/} requires NMDDNS v${NMDDNS_REQUIRED}+"; exit 2; }

ddns_nm_action() {
  # <interface> <action>
  local interface=$1 action=$2
  local config=${NMDDNS_CONFIG_PAT/@MATCH@/${interface}}

  nmddns_read_config "${config}" || return 0

  # run from NM
  nmg_debug "interface: ${interface} action: ${action}"

  [[ -e ${DDNS_STATE_DIR} ]] || {
    [[ ${NM_DISPATCHER_ACTION} ]] || {
      nmg_err "STATE_DIR ${DDNS_STATE_DIR} missing; run from\
 NetworkManager as dispatcher to create with correct permissions!"
      return 1
    }
    nmg_cmd mkdir -p "${DDNS_STATE_DIR}" || return
  }

  # use current ips on interface
  local addr="!${interface}"
  local state_pat=${NMDDNS_STATE_PAT/@MATCH@/${interface}}

  case ${action} in
    up|down)
      [[ ${action} == up ]] || addr=''
      nmddns_spawn_update_all "${action}" "${config}" "${addr}" "${addr}" \
                              "${state_pat}"
      ;;
    dhcp4-change)
      nmddns_spawn_update "${config}" "A" "${addr}" "${state_pat}"
      ;;
    dhcp6-change)
      nmddns_spawn_update "${config}" "AAAA" "${addr}" "${state_pat}"
      ;;
  esac

  return 0
}

ddns_helper_action() {
  local rc=0

  nmddns_required_config "${NMDDNSH_CONFIG-}"

  case ${NMDDNSH_ACTION} in
    update)
      nmddns_update "${NMDDNSH_RREC-}" "${NMDDNSH_VALUE-}" \
                    "${NMDDNSH_STATE-}" || rc=$?
      ;;
    up|down)
      nmddns_update_all "${NMDDNSH_ACTION}" "${NMDDNSH_ADDR4-}" \
                        "${NMDDNSH_ADDR6-}" "${NMDDNSH_STATE-}" || rc=$?
      ;;
    *)
      nmg_err "${0##*/} Unknown helper action ${NMDDNSH_ACTION}"
      rc=1
      ;;
  esac

  # ignore server unreachable
  (( rc == 25 )) && rc=0

  return ${rc}
}

function ddns_interface_rrec() {
  # <match> <rrec>
  local match=$1 rrec=$2 value='' rc=0

  # get state file pattern (used for per-config overrides)
  local state=${NMDDNS_STATE_PAT/@MATCH@-@RREC@/${match}-${rrec}}

  # get interface from match
  local intf=${match%-from-*}

  if [[ -e ${state} ]] && nmg::read value "" "${state}"; then

    # strip newline...
    value=${value%%$'\n'*}

    if [[ ${value} ]]; then
      # if ip-addresses, verify on interface
      if [[ ${rrec} =~ ^A|AAAA$ && ! ${value} =~ ^[!].+$ ]]; then
        # get values that are valid on <intf>, remove addrs if down
        local avail=() avals=() newvals=() tval

        # cleanup supplied values
        nmg::lowercase value "${value}"
        nmg::array avals "," "${value}"

        if [[ ${rrec} == A ]]; then
          nmddns::get_A_addrs avail "${intf}" \
                              "${DDNS_RREC_A_PRIVATE-}" || :
        else
          nmddns::get_AAAA_addrs avail "${intf}" \
                                 "${DDNS_RREC_AAAA_PRIVATE-}" || :
        fi

        # find intersection with available addrs
        for value in ${avals[*]+"${avals[@]}"}; do
          # strip any /prefix
          value=${value%%/*}
          for tval in ${avail[*]+"${avail[@]}"}; do
            [[ ${tval} == "${value}" ]] && { newvals+=("${value}"); break; }
          done
        done
        nmg::array_join value "," "${newvals[@]-}"
      fi
    fi
  fi

  nmddns_update "${rrec}" "${value}" || rc=$?

  # ignore server unreachable
  (( rc == 25 )) && rc=0

  return ${rc}
}

ddns_direct_cb() {
  # <file> <match> [ <interface> ]
  local config=$1 match=$2 intf=${3-} name rrec

  [[ ${match} ]] || return 0

  # if we have an interface, make sure we filter for it
  [[ ${intf} && ${intf} != "${match%-from-*}" ]] && return 0

  # load DDNS config
  nmddns_read_config "${config}" || return 0

  for name in "${!DDNS_RREC_@}"; do
    [[ ${name} =~ _NAME$ ]] || continue
    name=${name#DDNS_RREC_}
    rrec=${name%_NAME}
    [[ ${rrec} ]] || continue
    ddns_interface_rrec "${match}" "${rrec}"
  done

  return 0
}

ddns_direct() {
  # [ <interface> ]
  nmg::foreach_filematch "${NMDDNS_CONFIG_PAT}" "@MATCH@" ddns_direct_cb "$@"
}

if [[ ${1-} && ${2-} ]]; then

  case $2 in
    up|down|dhcp4-change|dhcp6-change)
      ddns_nm_action "$1" "$2" || exit
      ;;
  esac
elif [[ ${NMDDNSH_ACTION-} ]]; then

  # called from ddns-functions
  ddns_helper_action || exit
else

  # called from boot script
  ddns_direct "${1-}"
fi
: # for loading in tests
