#!/usr/bin/bash
# -*- mode: sh; sh-basic-offset: 2; indent-tabs-mode: nil; -*-
# vim:set ft=sh et sw=2 ts=2:
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Copyright (C) 2014-2025 Scott Shambarger
#
# 09-ddns v1.5.0 - NetworkManager dispatch for ipv4 Dynamic DNS updates
# Author: Scott Shambarger <devel@shambarger.net>
#
# 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
#
# External features:
#
#   This script may be used directly for a few key features.  The following
#   parameters may be passed to the script to perform these features.
#
#   help - show these instructions and config variables docs in a pager
#
#   config [ -a ] [ <INTERFACE>... ] - show configuration (of <INTERFACE>...)
#     If "-a" supplied, unset/empty variables with defaults are displayed.
#
# 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

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

# for logging
NMG_TAG=${NMG_TAG:-nmddns}
[[ -z ${NM_DISPATCHER_ACTION-} && -z ${NMDDNSH_ACTION-} ]] && {
  # run from shell, log to stderr
  # shellcheck disable=SC2034
  [[ ${LOGNAME-} ]] && nmg_log_stderr=1
  [[ $1 =~ ^help|-h|config$ ]] && _NMG_IGNORE_PROGS=1
}

# set NMUTILS early, and allow environment to override
NMUTILS=${NMUTILS:-/usr/share/nmutils}

# shellcheck disable=SC2034
NMDDNS_REQUIRED="1.6.0"

# load ddns-functions
NMDDNS=${NMDDNS:-${NMUTILS}/ddns-functions}
{ [[ -r ${NMDDNS} ]] && . "${NMDDNS}"; } || {
  echo 1>&2 "Unable to load ${NMDDNS} (set \$NMUTILS to it's dir)" && 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

  nmg_debug "ACTION: ${NMDDNSH_ACTION}"

  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_rep() { Report+="$1"$'\n'; }

ddns_config_intf() { # <file> <match> [ <interface> ]
  local file=$1 match=$2 intf=${3-} out
  match=${match%-from-*}
  [[ ${match} ]] || return 0
  shift 2
  [[ ${intf} && ${intf} != "${match}" ]] && return 0
  [[ ${match} != "${Intf}" ]] && {
    Intf=${match}; ddns_rep $'\n'"Interface ${Intf}"
  }
  if nmddns_read_config "${file}"; then
    # shellcheck disable=SC2059
    ddns_rep "  DDNS config \"${file}\""
    nmddns_get_config out "${Fmt}    %s=%s\n"
    Report+="${out}"
  else
    out="not found"
    [[ -e ${file} ]] && out="invalid"
    ddns_rep "  DDNS config \"${file}\" ($out)"
  fi
  nmddns_reset_config
}

ddns_config() {
  # [ -a ] [ <intf>... ]
  local Report='' Fmt='' Intf='' out intf
  # shellcheck disable=SC2034
  nmg_show_debug=''

  [[ ${1-} == "-a" ]] && { shift; Fmt="%-"; }
  Report+="Global config \"${NMCONF}/general.conf\""$'\n'
  nmg::get_config "out" "${Fmt}  %s=%s\n"
  Report+="${out}"
  nmddns_get_globals "out" "${Fmt}  %s=%s\n"
  Report+="${out}"

  if [[ ${1-} ]]; then
    for intf in "$@"; do
      nmg::foreach_filematch "${NMDDNS_CONFIG_PAT}" "@MATCH@" \
                             ddns_config_intf "${intf}"
      [[ ${intf} == "${Intf}" ]] ||
        ddns_rep $'\n'"Interface ${intf} not configured"
    done
  else
    nmg::foreach_filematch "${NMDDNS_CONFIG_PAT}" "@MATCH@" \
                           ddns_config_intf
  fi

  printf "%s" "${Report}"
}

ddns_help() {
  local out dout
  nmg::load_comment "${BASH_SOURCE[0]}" out 09-ddns "^# State file" || exit
  nmg::load_comment "${NMDDNS}" dout "^# Global Over" "NOTE:\\ exec" || exit
  out+=${dout/, see nmddns_read_config() above/}
  [[ ${PAGER} ]] || {
    for PAGER in less more cat; do
      command >/dev/null -v "${PAGER}" && break
    done
  }
  [[ ${PAGER} != cat ]] && { printf %s "${out}" | "${PAGER}"; return; }
  printf %s "${out}"
}

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_command() {
  # [ <command> | <interface> ] [ <interface> ]
  case $1 in
    config)
      shift
      ddns_config "$@"
      ;;
    help|-h)
      ddns_help
      ;;
    *)
      [[ ${1-} == direct ]] && shift
      nmg_debug "DIRECT: ${1-}"

      nmg::foreach_filematch "${NMDDNS_CONFIG_PAT}" "@MATCH@" \
                             ddns_direct_cb "$@"
      ;;
  esac
}

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

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

  # called from ddns-functions
  ddns_helper_action || exit

elif [[ -z ${NM_DISPATCHER_ACTION-} ]]; then

  # called from boot script/command-line
  ddns_command "$@"
fi
: # for loading in tests
