#!/bin/sh ############################################################################### # dyndns.sh # Dynamic DNS backend management script # This script manages host entries for a personal dynamic DNS service. It # supports database-backed hostname registration, validation and removal, and # provides helper functions for DNS zone update workflows. # Features: # - Add, check and remove dynDNS hostnames # - Load configuration from a vars file # - Maintain log and temporary runtime directories # - Validate database connectivity before command execution # - POSIX-compatible operation for cron and service integration # Authors: Stephan Düsterhaupt, Ivo Noack aka Insonic # Copyright (c) 2018-2026 CB-601 - the open tec Elevator # License: MIT ############################################################################### # MIT License # Copyright (c) 2026 CB-601 - the open tec Elevator # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # SDuesterhaupt: 2026-06-18 - Show general usage information and an overview # of available commands. _usage() { _log "Enter the function '_usage()'..." 1 # Collect/show dir status: err_source="Not defined: vars autodetect failed and no value provided" work_dir=${DYNDNS:-$err_source} zone_name=${DYNDNS_BIND_ZONE:-$err_source} printf '%s\n' \ "dynDNS usage and overview" \ "" \ "USAGE: dyndns [options] COMMAND [command-options]" \ "" \ "A list of commands is shown below. To get detailed usage and help for a command, run:" \ " dyndns help COMMAND" \ "" \ "For a listing of options that can be supplied before the command, use:" \ " dyndns help options" \ "" \ "Commands:" \ " add-hostname [ cmd-opts ]" \ " check-hostname [ cmd-opts ]" \ " remove-hostname [ cmd-opts ]" \ " update-zone [ cmd-opts ]" \ "" \ "DIRECTORY STATUS (commands would take effect on these locations)" \ " DYNDNS: $work_dir" \ " ZONE: $zone_name" \ "" _log "Leave the function '_usage()'..." 1 return 0 } #=> _usage() # SDuesterhaupt: 2026-06-18 - Show command-specific help text or global option help. # When called without arguments, show general usage or command-specific help. _cmd_help() { text="" opts="" case "$1" in add-hostname) text="add-hostname [ cmd-opts]\n Add a hostname to a DNS zone." ;; check-hostname) text="check-hostname [ cmd-opts]\n Check a hostname of a DNS zone." ;; remove-hostname) text="remove-hostname [ cmd-opts]\n Remove a hostname from a DNS zone." ;; update-zone) text="update-zone [ cmd-opts]\n Update a DNS zone." ;; options) _opt_usage return 0 ;; "") _usage return 0 ;; *) text="Unknown command: '$1' (try without command for a list of commands)" ;; esac # Display the help text [ -n "$text" ] && printf '%b\n' "$text" if [ -n "$opts" ]; then printf '\n%s\n' "cmd-opts is an optional set of command options from this list:" printf '%s\n' "$opts" fi return 0 } #=> _cmd_help() # SDuesterhaupt: 2026-06-18 - Print descriptions of global command-line options. _opt_usage() { printf '%s\n' \ "dynDNS Global Option Flags" \ "" \ "The following options may be provided before the command. Options specified" \ "at runtime override env-vars and any 'vars' file in use. Unless noted," \ "non-empty values to options are mandatory." \ "" \ "General options:" \ " --vars=FILE define a specific 'vars' file to use for dynDNS config" \ "" \ "Add hostname options:" \ " --hostname=ARG set a fully qualified hostname" \ " --email=ARG set an e-mail address" \ " --pass=ARG set a password" \ "" \ "Update zone options:" \ " --key=ARG set the TSIG key to grant the nameserver access" \ " --zone=ARG set the DNS zone name (example.com)" \ "" return 0 } #=> _opt_usage() # SDuesterhaupt: 2026-06-18 - Abort execution with an error message and exit code. _die() { msg=$1 code=${2:-1} printf '\n%s\n%s\n' "dynDNS error:" "$msg" >&2 if [ "$code" -ne 0 ] && [ -n "$DYNDNS_LOG_FILE" ]; then ts=$(date '+%Y-%m-%d %H:%M:%S') printf '%s dyndns [ERROR] %s\n' "$ts" "$msg" >>"$DYNDNS_LOG_FILE" fi exit "$code" } #=> _die() # SDuesterhaupt: 2026-06-18 - Print an informational notice unless batch mode is enabled. notice() { if [ -z "$DYNDNS_BATCH" ]; then printf '\n%s\n' "$1" fi return 0 } #=> notice() # SDuesterhaupt: 2026-06-18 - Create a temporary file in the session-specific temp directory. # The function expects DYNDNS_TEMP_DIR_session to be initialized by _vars_setup(). # It returns the path of the created file on stdout. dyndns_mktemp() { [ -n "$DYNDNS_TEMP_DIR_session" ] \ || _die "DYNDNS_TEMP_DIR_session not initialized!" 1 [ -d "$DYNDNS_TEMP_DIR_session" ] \ || mkdir -p "$DYNDNS_TEMP_DIR_session" \ || _die "Could not create temporary directory '$DYNDNS_TEMP_DIR_session'" 1 template="$DYNDNS_TEMP_DIR_session/tmp.XXXXXX" tempfile=$(mktemp "$template") || return 1 # Work around mktemp variants that incorrectly return the literal template. if [ "$template" = "$tempfile" ]; then tempfile=$(mktemp -du "$tempfile") || return 1 : >"$tempfile" || return 1 fi printf '%s\n' "$tempfile" return 0 } #=> dyndns_mktemp() # SDuesterhaupt: 2026-06-18 - Remove temporary files and restore terminal echo state. cleanup() { [ -n "$DYNDNS_TEMP_DIR_session" ] && rm -rf "$DYNDNS_TEMP_DIR_session" stty echo 2>/dev/null || true printf '\n' return 0 } #=> cleanup() # SDuesterhaupt: 2026-06-18 - Verify essential DYNDNS environment variables are defined # Checks the presence of critical env vars for dynDNS runtime. # Exits fatally if any required variable is undefined. _vars_source_check() { _log "Enter the function '_vars_source_check()'..." 1 [ -n "$DYNDNS" ] || _die "DYNDNS env-var undefined" 5 [ -n "$DYNDNS_TEMP_DIR" ] || _die "DYNDNS_TEMP_DIR env-var undefined" 5 [ -n "$DYNDNS_LOG_DIR" ] || _die "DYNDNS_LOG_DIR env-var undefined" 5 [ -n "$DYNDNS_LOG_FILE" ] || _die "DYNDNS_LOG_FILE env-var undefined" 5 [ -n "$DYNDNS_LOG_LEVEL" ] || _die "DYNDNS_LOG_LEVEL env-var undefined" 5 [ -n "$DYNDNS_DB_PROGRAM" ] || _die "DYNDNS_DB_PROGRAM env-var undefined" 5 [ -n "$DYNDNS_SQL_HOST" ] || _die "DYNDNS_SQL_HOST env-var undefined" 5 [ -n "$DYNDNS_SQL_DATABASE" ] || _die "DYNDNS_SQL_DATABASE env-var undefined" 5 [ -n "$DYNDNS_SQL_USER" ] || _die "DYNDNS_SQL_USER env-var undefined" 5 [ -n "$DYNDNS_SQL_PASS" ] || _die "DYNDNS_SQL_PASS env-var undefined" 5 [ -n "$DYNDNS_BIND_ZONE" ] || _die "DYNDNS_BIND_ZONE env-var undefined" 5 [ -n "$DYNDNS_BIND_ZONE_KEY" ] || _die "DYNDNS_BIND_ZONE_KEY env-var undefined" 5 [ -n "$DYNDNS_BIND_ZONE_DIR" ] || _die "DYNDNS_BIND_ZONE_DIR env-var undefined" 5 [ -n "$DYNDNS_BIND_ZONE_FILE" ] || _die "DYNDNS_BIND_ZONE_FILE env-var undefined" 5 _log "Leave the function '_vars_source_check()'..." 1 return 0 } #=> _vars_source_check() # SDuesterhaupt: 2026-06-18 - Verify dynDNS runtime directories, files and external tools. # Checks existence of required directories/files and command availability. # Calls _vars_source_check internally. _verify_runtime_init() { _log "Enter the function '_verify_runtime_init()'..." 1 help_note="Run dyndns without commands for usage and command help." _vars_source_check [ -d "$DYNDNS_LOG_DIR" ] || _die "DYNDNS_LOG_DIR missing. $help_note" 5 [ -d "$DYNDNS_BIND_ZONE_DIR" ] || _die "DYNDNS_BIND_ZONE_DIR missing. $help_note" 5 #[ -f "$DYNDNS_BIND_ZONE_FILE" ] || _die "DYNDNS_BIND_ZONE_FILE missing. $help_note" 5 [ -x "$DYNDNS_DB_PROGRAM" ] \ || command -v "$DYNDNS_DB_PROGRAM" >/dev/null 2>&1 \ || _die "Database client '$DYNDNS_DB_PROGRAM' not available. $help_note" 5 command -v rndc >/dev/null 2>&1 || _die "rndc not available. $help_note" 5 command -v sed >/dev/null 2>&1 || _die "sed not available. $help_note" 5 command -v grep >/dev/null 2>&1 || _die "grep not available. $help_note" 5 command -v awk >/dev/null 2>&1 || _die "awk not available. $help_note" 5 command -v nsupdate >/dev/null 2>&1 || _die "nsupdate not available. $help_note" 5 _log "Leave the function '_verify_runtime_init()'..." 1 return 0 } #=> _verify_runtime_init() # SDuesterhaupt: 2026-06-18 - Ensure the log directory exists before logging starts. _init_log() { if [ ! -d "$DYNDNS_LOG_DIR" ]; then printf "The log directory '%s' doesn't exist. Try to create it...\n" \ "$DYNDNS_LOG_DIR" if mkdir -p "$DYNDNS_LOG_DIR" 2>/dev/null; then printf "Log directory '%s' created.\n" "$DYNDNS_LOG_DIR" else _die "Could not create log directory '$DYNDNS_LOG_DIR'" 1 fi fi return 0 } #=> _init_log() # SDuesterhaupt: 2026-06-18 - Initialize database connection and verify access _init_DBconnection() { _log "Enter the function '_init_DBconnection()'..." 1 [ -n "$DYNDNS_SQL_HOST" ] || _die "DYNDNS_SQL_HOST is not set." 5 [ -n "$DYNDNS_SQL_DATABASE" ] || _die "DYNDNS_SQL_DATABASE is not set." 5 [ -n "$DYNDNS_SQL_USER" ] || _die "DYNDNS_SQL_USER is not set." 5 [ -n "$DYNDNS_SQL_PASS" ] || _die "DYNDNS_SQL_PASS is not set." 5 if _db_exec 'SELECT 1;' >/dev/null 2>&1; then _log "The database '$DYNDNS_SQL_DATABASE' is accessible." 2 _log "Leave the function '_init_DBconnection()'..." 1 return 0 fi printf '%s\n' "TRACE: _db_exec failed in _init_DBconnection" >&2 _die "The database '$DYNDNS_SQL_DATABASE' is not accessible." 5 } #=> _init_DBconnection() # SDuesterhaupt: 2026-06-18 - Load configuration from a vars file and apply built-in defaults. # # Search order: # 1. File specified by DYNDNS_VARS_FILE # 2. File named 'vars' below the configured DYNDNS directory # 3. File named 'vars' next to the script itself # # Existing environment variables take precedence over built-in defaults. # The function also initializes a per-run session temp directory. _vars_setup() { # Try to locate a 'vars' file in order of location preference. # If one is found, source it prog_file=$0 prog_dir=$(dirname "$(readlink -f "$prog_file" 2>/dev/null || printf '%s' "$prog_file")") vars="" [ -f "$DYNDNS_VARS_FILE" ] && vars=$DYNDNS_VARS_FILE [ -z "$vars" ] && [ -n "$DYNDNS" ] && [ -f "$DYNDNS/vars" ] && vars=$DYNDNS/vars [ -z "$vars" ] && [ -f "$prog_dir/vars" ] && vars=$prog_dir/vars if [ -z "$DYNDNS_NO_VARS" ] && [ -n "$vars" ]; then DYNDNS_CALLER=1 export DYNDNS_CALLER # shellcheck source=/dev/null . "$vars" notice "Note: using dynDNS configuration from: $vars" fi # Core paths set_var DYNDNS "$prog_dir" set_var DYNDNS_TEMP_DIR "$DYNDNS/tmp" # Logging set_var DYNDNS_LOG_DIR "$DYNDNS/log" set_var DYNDNS_LOG_FILE "$DYNDNS_LOG_DIR/dyndns_${DYNDNS_BIND_ZONE:-default}.log" set_var DYNDNS_LOG_UPDATE_FILE "$DYNDNS_LOG_DIR/lastZoneUpdate_${DYNDNS_BIND_ZONE:-default}.log" set_var DYNDNS_LOG_LEVEL 3 set_var DYNDNS_BATCH "" # Database access set_var DYNDNS_SQL_HOST "localhost" set_var DYNDNS_SQL_GROUP "dyndns" set_var DYNDNS_SQL_DATABASE "dyndns_database" set_var DYNDNS_SQL_USER "dyndns_user" set_var DYNDNS_SQL_PASS "strong_passphrase" set_var DYNDNS_SQL_OPTIONS "" set_var DYNDNS_DB_PROGRAM "/usr/bin/mariadb" # DNS/BIND defaults set_var DYNDNS_BIND_SERVER "127.0.0.1" set_var DYNDNS_BIND_SERVICE "named.service" set_var DYNDNS_BIND_ZONE "example24.com" set_var DYNDNS_BIND_ZONE_KEY "keyfile" set_var DYNDNS_BIND_ZONE_DIR "/var/named/zones" set_var DYNDNS_BIND_ZONE_FILE "$DYNDNS_BIND_ZONE_DIR/$DYNDNS_BIND_ZONE.zone" set_var DYNDNS_BIND_ZONE_FILE_SIGNED "$DYNDNS_BIND_ZONE_DIR/$DYNDNS_BIND_ZONE.zone.signed" set_var DYNDNS_BIND_ZONE_TTL 300 # Create a per-run temp directory name. The directory itself may be created # lazily later by dyndns_mktemp(). if [ -z "$DYNDNS_TEMP_DIR_session" ]; then if [ -d "$DYNDNS_TEMP_DIR" ]; then DYNDNS_TEMP_DIR_session=$(mktemp -du "$DYNDNS_TEMP_DIR/dyn-dns-$$.XXXXXX") else mkdir -p "$DYNDNS_TEMP_DIR" \ || _die "Cannot create $DYNDNS_TEMP_DIR (permission?)" 1 DYNDNS_TEMP_DIR_session=$(mktemp -du "$DYNDNS_TEMP_DIR/dyn-dns-$$.XXXXXX") rm -rf "$DYNDNS_TEMP_DIR" fi export DYNDNS_TEMP_DIR_session fi return 0 } #=> _vars_setup() # SDuesterhaupt: 2026-06-18 - Set an environment variable to a default value if it # is currently unset or empty. set_var() { var=$1 shift eval "$var=\${$var:-\"\$*\"}" eval "export $var" } #=> set_var() # SDuesterhaupt: 2026-06-18 - Write a message to the dynDNS log file. # # Severity levels: # 0 -> OFF # 1 -> DEBUG # 2 -> INFO # 3 -> WARNING # 4 -> ERROR # 5 -> CRITICAL # # @param1: Log message. # @param2: Severity level. _log() { msg=$1 level=${2:-0} set -- "" "DEBUG" "INFO" "WARNING" "ERROR" "CRITICAL" shift level_name="" if [ "$level" -ge 1 ] 2>/dev/null; then level_name=$(eval "printf '%s' \"\${$level}\"") fi level_tag="[$level_name]" level_tag_len=${#level_tag} pad_width=10 pad_count=$((pad_width - level_tag_len)) [ "$pad_count" -lt 1 ] && pad_count=1 pad=$(printf '%*s' "$pad_count" '') # Write the log entry only if the message is not empty if [ -n "$msg" ] && [ "$level" -ge "$DYNDNS_LOG_LEVEL" ] 2>/dev/null; then ts=$(date '+%Y-%m-%d %H:%M:%S') printf '%s dyndns %s%s%s\n' \ "$ts" "$level_tag" "$pad" "$msg" >>"$DYNDNS_LOG_FILE" fi if [ "$level" -ge 3 ] 2>/dev/null; then printf '%s\n' "$msg" >&2 fi return 0 } #=> _log() # SDuesterhaupt: 2026-06-18 - Check whether DYNDNS_MEMBER_HOSTNAME is a valid hostname # # Returns: # 0 if valid # 1 if invalid _hostname_is_valid() { _log "Enter the function '_hostname_is_valid()'..." 1 hostname=$DYNDNS_MEMBER_HOSTNAME case "$hostname" in ''|.*|*..*|*.-*|*-.|*-|*.) _log "The expression '$hostname' is not a valid hostname." 3 _log "Leave the function '_hostname_is_valid()'..." 1 return 1 ;; esac if printf '%s\n' "$hostname" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z]{2,}$'; then _log "The expression '$hostname' is a valid hostname." 2 _log "Leave the function '_hostname_is_valid()'..." 1 return 0 fi _log "The expression '$hostname' is not a valid hostname." 3 _log "Leave the function '_hostname_is_valid()'..." 1 return 1 } #=> _hostname_is_valid() # SDuesterhaupt: 2026-06-18 - Extract base domain from DYNDNS_MEMBER_HOSTNAME # Prints: # example.org # # Returns: # 0 on success # 1 on failure _hostname_get_domain() { _log "Enter the function '_hostname_get_domain()'..." 1 hostname=$DYNDNS_MEMBER_HOSTNAME if ! _hostname_is_valid >/dev/null 2>&1; then _log "Cannot extract domain from invalid hostname '$hostname'." 4 _log "Leave the function '_hostname_get_domain()'..." 1 return 1 fi domain=$(printf '%s\n' "$hostname" | sed 's/^.*\.\([^.][^.]*\.[^.][^.]*\)$/\1/') if [ -n "$domain" ] && [ "$domain" != "$hostname" ]; then printf '%s\n' "$domain" _log "Leave the function '_hostname_get_domain()'..." 1 return 0 fi _log "Could not extract domain from '$hostname'." 4 _log "Leave the function '_hostname_get_domain()'..." 1 return 1 } #=> _hostname_get_domain() # SDuesterhaupt: 2026-06-18 - Extract subdomain from DYNDNS_MEMBER_HOSTNAME # # Prints: # host or sub.host # # Returns: # 0 on success # 1 on failure _hostname_get_subdomain() { _log "Enter the function '_hostname_get_subdomain()'..." 1 hostname=$DYNDNS_MEMBER_HOSTNAME if ! _hostname_is_valid >/dev/null 2>&1; then _log "Cannot extract subdomain from invalid hostname '$hostname'." 4 _log "Leave the function '_hostname_get_subdomain()'..." 1 return 1 fi subdomain=$(printf '%s\n' "$hostname" | sed 's/\.[^.][^.]*\.[^.][^.]*$//') if [ -n "$subdomain" ] && [ "$subdomain" != "$hostname" ]; then printf '%s\n' "$subdomain" _log "Leave the function '_hostname_get_subdomain()'..." 1 return 0 fi _log "Could not extract subdomain from '$hostname'." 4 _log "Leave the function '_hostname_get_subdomain()'..." 1 return 1 } #=> _hostname_get_subdomain() # SDuesterhaupt: 2026-06-18 - Compatibility wrapper around hostname helper functions # # @param1: check | get # @param2: hostname | domain | subdomain # # Returns: # 0 on success # 1 on failure _Hostname() { _log "Enter the function '_Hostname()'..." 1 method=$1 part=$2 case "$method" in check) case "$part" in hostname|fqdn) _hostname_is_valid return $? ;; *) _die "Invalid action '$part' in function _Hostname()." 5 ;; esac ;; get) case "$part" in domain) _hostname_get_domain return $? ;; subdomain) _hostname_get_subdomain return $? ;; *) _die "Invalid action '$part' in function _Hostname()." 5 ;; esac ;; *) _die "Invalid method '$method' in function _Hostname()." 5 ;; esac } #=> _Hostname() # SDuesterhaupt: 2026-06-18 - Build the mysql command line # # Prints a shell-escaped mysql command line to stdout. # Authentication is taken either from a mysql option group suffix # or from explicit user/password settings. _db_cmd() { if [ -n "$DYNDNS_SQL_GROUP" ]; then printf '%s\n' \ "\"$DYNDNS_DB_PROGRAM\" --defaults-group-suffix=$DYNDNS_SQL_GROUP $DYNDNS_SQL_OPTIONS -h \"$DYNDNS_SQL_HOST\" -D \"$DYNDNS_SQL_DATABASE\" -Ns" else printf '%s\n' \ "\"$DYNDNS_DB_PROGRAM\" $DYNDNS_SQL_OPTIONS -h \"$DYNDNS_SQL_HOST\" -u \"$DYNDNS_SQL_USER\" -p\"$DYNDNS_SQL_PASS\" -D \"$DYNDNS_SQL_DATABASE\" -Ns" fi return 0 } #=> _db_cmd() # SDuesterhaupt: 2026-06-18 - Execute an SQL statement # # @param1: SQL statement # # Writes query result to stdout and returns mysql exit code. _db_exec() { sql=$1 if [ -z "$sql" ]; then _log "Empty SQL statement passed to _db_exec()." 4 return 1 fi db_cmd=$(_db_cmd) || return 1 eval "$db_cmd -e \"\$sql\"" return $? } #=> _db_exec() # SDuesterhaupt: 2026-06-18 - Execute a scalar SQL query # # @param1: SQL query # # Prints query result to stdout. # Returns 0 on success, 1 on failure. _db_query_scalar() { sql=$1 if [ -z "$sql" ]; then _log "Empty SQL statement passed to _db_query_scalar()." 4 return 1 fi _db_exec "$sql" | awk 'NR == 1 { print; exit }' return $? } #=> _db_query_scalar() # SDuesterhaupt: 2026-06-18 - Escape string for safe SQL single-quoted usage # # @param1: Raw string # # Prints escaped string to stdout. _sql_escape() { printf '%s' "$1" | sed "s/'/''/g" } #=> _sql_escape() # SDuesterhaupt: 2026-06-20 - Check whether hostname child tables are protected by # foreign keys with ON DELETE CASCADE. # # Required relationships: # connections.id -> members.id # login_attempts.id -> members.id # # Returns: # 0 if both foreign keys exist with DELETE_RULE = 'CASCADE' # 1 otherwise _hostname_db_has_delete_cascade() { result=$(_db_query_scalar " SELECT COUNT(*) FROM ( SELECT kcu.TABLE_NAME, kcu.COLUMN_NAME, kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME, rc.DELETE_RULE FROM information_schema.KEY_COLUMN_USAGE kcu JOIN information_schema.REFERENTIAL_CONSTRAINTS rc ON rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME WHERE kcu.CONSTRAINT_SCHEMA = '$(_sql_escape "$DYNDNS_SQL_DATABASE")' AND ( (kcu.TABLE_NAME = 'connections' AND kcu.COLUMN_NAME = 'id' AND kcu.REFERENCED_TABLE_NAME = 'members' AND kcu.REFERENCED_COLUMN_NAME = 'id') OR (kcu.TABLE_NAME = 'login_attempts' AND kcu.COLUMN_NAME = 'id' AND kcu.REFERENCED_TABLE_NAME = 'members' AND kcu.REFERENCED_COLUMN_NAME = 'id') ) AND rc.DELETE_RULE = 'CASCADE' ) AS fk_check; " 2>/dev/null) [ "$result" = "2" ] } #=> _hostname_db_has_delete_cascade() # SDuesterhaupt: 2026-06-18 - Generate SHA-512 hex digest # # @param1: Raw string # # Prints digest to stdout. _sha512_hex() { if command -v sha512sum >/dev/null 2>&1; then printf '%s' "$1" | sha512sum | awk '{print $1}' else printf '%s' "$1" | openssl dgst -sha512 | sed 's/^.*= //' fi } #=> _sha512_hex() # SDuesterhaupt: 2026-06-18 - Manage hostname entries in the database # # @param1: Method: add | check | remove # # Uses: # DYNDNS_MEMBER_HOSTNAME # DYNDNS_MEMBER_EMAIL # DYNDNS_MEMBER_PASS # # Returns: # 0 on success # 1 on lookup or operation failure _HostnameDB() { _log "Enter the function '_HostnameDB()'..." 1 method=$1 return_flag=1 member_id="" db_count=0 _log "Method '$method'..." 1 case "$method" in add) _log "Add the hostname '$DYNDNS_MEMBER_HOSTNAME' to the database..." 2 # Get username = subdomain username=$(_Hostname get subdomain) || { _log "Could not extract subdomain from '$DYNDNS_MEMBER_HOSTNAME'." 4 _log "Leave the function '_HostnameDB()'..." 1 return 1 } # Get domain domain=$(_Hostname get domain) || { _log "Could not extract domain from '$DYNDNS_MEMBER_HOSTNAME'." 4 _log "Leave the function '_HostnameDB()'..." 1 return 1 } # Create a random salt random_seed=$(openssl rand -hex 16 2>/dev/null) || { _log "Could not generate random salt seed." 4 _log "Leave the function '_HostnameDB()'..." 1 return 1 } # Create salted password random_salt=$(_sha512_hex "$random_seed") password_hash=$(_sha512_hex "${DYNDNS_MEMBER_PASS}.${random_salt}") # Get timestamp timestamp=$(date +%s) sql_username=$(_sql_escape "$username") sql_domain=$(_sql_escape "$domain") sql_email=$(_sql_escape "$DYNDNS_MEMBER_EMAIL") sql_password_hash=$(_sql_escape "$password_hash") sql_random_salt=$(_sql_escape "$random_salt") sql_hostname=$(_sql_escape "$DYNDNS_MEMBER_HOSTNAME") # Add new entry to database.members if _db_exec " INSERT INTO members (username, domain, email, password, salt, timestamp) VALUES ('$sql_username', '$sql_domain', '$sql_email', '$sql_password_hash', '$sql_random_salt', '$timestamp'); " >/dev/null 2>&1; then _log "The hostname '$DYNDNS_MEMBER_HOSTNAME' was added to '$DYNDNS_SQL_DATABASE.members'." 2 else _log "The hostname '$DYNDNS_MEMBER_HOSTNAME' could not be added to '$DYNDNS_SQL_DATABASE.members'." 4 _log "Leave the function '_HostnameDB()'..." 1 return 1 fi # Get the id from database.members member_id=$(_db_query_scalar " SELECT id FROM members WHERE CONCAT(members.username, '.', members.domain) = '$sql_hostname' LIMIT 1; " 2>/dev/null) if [ -z "$member_id" ]; then _log "Could not determine member id for '$DYNDNS_MEMBER_HOSTNAME'." 4 _log "Leave the function '_HostnameDB()'..." 1 return 1 fi # Add new entry to database.connections if _db_exec " INSERT INTO connections (id, IP, IPv6_flag, IP_locked, timestamp) VALUES ('$member_id', '127.0.0.1', '0', '0', '$timestamp'); " >/dev/null 2>&1; then _log "The id '$member_id' was added to '$DYNDNS_SQL_DATABASE.connections'." 2 else _log "The id '$member_id' could not be added to '$DYNDNS_SQL_DATABASE.connections'." 4 _log "Leave the function '_HostnameDB()'..." 1 return 1 fi # Add new entry to database.login_attempts if _db_exec " INSERT INTO login_attempts (id, count, timestamp) VALUES ('$member_id', '0', '$timestamp'); " >/dev/null 2>&1; then _log "The id '$member_id' was added to '$DYNDNS_SQL_DATABASE.login_attempts'." 2 return_flag=0 else _log "The id '$member_id' could not be added to '$DYNDNS_SQL_DATABASE.login_attempts'." 4 return_flag=1 fi ;; check) _log "Check whether the hostname '$DYNDNS_MEMBER_HOSTNAME' is stored in the database..." 2 sql_hostname=$(_sql_escape "$DYNDNS_MEMBER_HOSTNAME") # Count the member IDs in the database where domain="$DYNDNS_MEMBER_HOSTNAME" db_count=$(_db_query_scalar " SELECT COUNT(id) FROM members WHERE CONCAT(members.username, '.', members.domain) = '$sql_hostname'; " 2>/dev/null) case "$db_count" in ''|*[!0-9]*) _log "Could not determine database count for '$DYNDNS_MEMBER_HOSTNAME'." 4 return_flag=1 ;; *) if [ "$db_count" -ge 1 ]; then _log "The hostname '$DYNDNS_MEMBER_HOSTNAME' is stored in the database." 2 return_flag=0 else _log "The hostname '$DYNDNS_MEMBER_HOSTNAME' could not be found in database." 2 return_flag=1 fi ;; esac ;; remove) _log "Remove the hostname '$DYNDNS_MEMBER_HOSTNAME' from the database..." 2 sql_hostname=$(_sql_escape "$DYNDNS_MEMBER_HOSTNAME") member_id=$(_db_query_scalar " SELECT id FROM members WHERE CONCAT(members.username, '.', members.domain) = '$sql_hostname' LIMIT 1; " 2>/dev/null) if [ -z "$member_id" ]; then _log "No database id found for hostname '$DYNDNS_MEMBER_HOSTNAME'." 3 _log "Leave the function '_HostnameDB()'..." 1 return 1 fi if _hostname_db_has_delete_cascade; then _log "Detected ON DELETE CASCADE for hostname child tables. Using parent delete path." 2 if _db_exec "DELETE FROM members WHERE id = '$member_id';" >/dev/null 2>&1; then _log "The hostname '$DYNDNS_MEMBER_HOSTNAME' was removed from '$DYNDNS_SQL_DATABASE.members'." 2 return_flag=0 else _log "The hostname '$DYNDNS_MEMBER_HOSTNAME' could not be removed from '$DYNDNS_SQL_DATABASE.members'." 4 return_flag=1 fi else _log "ON DELETE CASCADE not detected. Using compatibility fallback delete path." 3 if _db_exec "DELETE FROM connections WHERE id = '$member_id';" >/dev/null 2>&1; then _log "The id '$member_id' was removed from '$DYNDNS_SQL_DATABASE.connections'." 2 else _log "The id '$member_id' could not be removed from '$DYNDNS_SQL_DATABASE.connections'." 4 _log "Leave the function '_HostnameDB()'..." 1 return 1 fi if _db_exec "DELETE FROM login_attempts WHERE id = '$member_id';" >/dev/null 2>&1; then _log "The id '$member_id' was removed from '$DYNDNS_SQL_DATABASE.login_attempts'." 2 else _log "The id '$member_id' could not be removed from '$DYNDNS_SQL_DATABASE.login_attempts'." 4 _log "Leave the function '_HostnameDB()'..." 1 return 1 fi if _db_exec "DELETE FROM members WHERE id = '$member_id';" >/dev/null 2>&1; then _log "The hostname '$DYNDNS_MEMBER_HOSTNAME' was removed from '$DYNDNS_SQL_DATABASE.members'." 2 return_flag=0 else _log "The hostname '$DYNDNS_MEMBER_HOSTNAME' could not be removed from '$DYNDNS_SQL_DATABASE.members'." 4 return_flag=1 fi fi ;; *) _die "Invalid method '$method' in function _HostnameDB()." 5 ;; esac _log "Leave the function '_HostnameDB()'..." 1 return "$return_flag" } #=> _HostnameDB() # SDuesterhaupt: 2026-06-18 - Add a hostname if it is valid and currently unused # # Returns: # 0 on success # 1 on failure _add_hostname() { _log "Enter the function '_add_hostname()'..." 1 # Check whether the hostname is a valid FQDN. if ! _hostname_is_valid >/dev/null 2>&1; then _log "The provided hostname '$DYNDNS_MEMBER_HOSTNAME' is not valid. Cannot add it." 3 _log "Leave the function '_add_hostname()'..." 1 return 1 fi # Check whether DYNDNS_MEMBER_EMAIL is not empty. [ -n "$DYNDNS_MEMBER_EMAIL" ] || { _log "The member e-mail address is empty. Cannot add hostname '$DYNDNS_MEMBER_HOSTNAME'." 4 _log "Leave the function '_add_hostname()'..." 1 return 1 } # Check whether DYNDNS_MEMBER_PASS is not empty. [ -n "$DYNDNS_MEMBER_PASS" ] || { _log "The member password is empty. Cannot add hostname '$DYNDNS_MEMBER_HOSTNAME'." 4 _log "Leave the function '_add_hostname()'..." 1 return 1 } # Check whether the hostname is still free. if _HostnameDB check; then _log "Cannot add the hostname '$DYNDNS_MEMBER_HOSTNAME'. The domain already exists." 2 _log "Leave the function '_add_hostname()'..." 1 return 1 fi # Add hostname to database. if _HostnameDB add; then _log "Hostname '$DYNDNS_MEMBER_HOSTNAME' was added successfully." 2 _log "Leave the function '_add_hostname()'..." 1 return 0 fi _log "Hostname '$DYNDNS_MEMBER_HOSTNAME' could not be added." 4 _log "Leave the function '_add_hostname()'..." 1 return 1 } #=> _add_hostname() # SDuesterhaupt: 2026-06-18 - Check whether a valid hostname exists in the database # # Returns: # 0 if hostname exists # 1 if hostname is invalid or absent _check_hostname() { _log "Enter the function '_check_hostname()'..." 1 # Check whether the hostname is a valid FQDN. if ! _hostname_is_valid >/dev/null 2>&1; then _log "The provided hostname '$DYNDNS_MEMBER_HOSTNAME' is not valid. Cannot check it." 3 _log "Leave the function '_check_hostname()'..." 1 return 1 fi # Check whether the hostname is stored in the database if _HostnameDB check; then _log "Hostname '$DYNDNS_MEMBER_HOSTNAME' exists in the database." 2 _log "Leave the function '_check_hostname()'..." 1 return 0 fi _log "Hostname '$DYNDNS_MEMBER_HOSTNAME' does not exist in the database." 2 _log "Leave the function '_check_hostname()'..." 1 return 1 } #=> _check_hostname() # SDuesterhaupt: 2026-06-18 - Remove a hostname if it is valid and exists # # Returns: # 0 on success # 1 on failure _remove_hostname() { _log "Enter the function '_remove_hostname()'..." 1 # Check whether the hostname is a valid FQDN. if ! _hostname_is_valid >/dev/null 2>&1; then _log "The provided hostname '$DYNDNS_MEMBER_HOSTNAME' is not valid. Cannot remove it." 3 _log "Leave the function '_remove_hostname()'..." 1 return 1 fi # Check whether the hostname exists if ! _HostnameDB check; then _log "Cannot remove the hostname '$DYNDNS_MEMBER_HOSTNAME'. The domain does not exist." 2 _log "Leave the function '_remove_hostname()'..." 1 return 1 fi # Remove the hostname from database if _HostnameDB remove; then _log "Hostname '$DYNDNS_MEMBER_HOSTNAME' was removed successfully." 2 _log "Leave the function '_remove_hostname()'..." 1 return 0 fi _log "Hostname '$DYNDNS_MEMBER_HOSTNAME' could not be removed." 4 _log "Leave the function '_remove_hostname()'..." 1 return 1 } #=> _remove_hostname() # SDuesterhaupt: 2026-06-18 - Get dynamic DNS records from database # # Output format: # subdomainhostnameiprecord_type # # Returns: # 0 on success # 1 on failure _zone_db_records_get() { _log "Enter the function '_zone_db_records_get()'..." 1 _db_exec " SELECT members.username, CONCAT(members.username, '.', members.domain) AS hostname, connections.IP, CASE WHEN connections.IPv6_flag = 1 THEN 'AAAA' ELSE 'A' END AS record_type FROM connections JOIN members ON connections.id = members.id WHERE members.domain = '$DYNDNS_BIND_ZONE' AND connections.IP_locked = 0; " 2>/dev/null rc=$? if [ "$rc" -ne 0 ]; then _log "Could not obtain DNS records from database for zone '$DYNDNS_BIND_ZONE'." 4 _log "Leave the function '_zone_db_records_get()'..." 1 return 1 fi _log "Leave the function '_zone_db_records_get()'..." 1 return 0 } #=> _zone_db_records_get() # SDuesterhaupt: 2026-06-18 - Extract existing A and AAAA records from zone file # # Output format: # subdomainhostnameiprecord_type # # Returns: # 0 on success # 1 on failure _zone_file_records_get() { _log "Enter the function '_zone_file_records_get()'..." 1 if [ ! -f "$DYNDNS_BIND_ZONE_FILE" ]; then _log "The file '$DYNDNS_BIND_ZONE_FILE' is not available." 4 _log "Leave the function '_zone_file_records_get()'..." 1 return 1 fi awk -v zone="$DYNDNS_BIND_ZONE" ' BEGIN { OFS = "\t" origin = "" } /^[[:space:]]*$/ { next } /^[[:space:]]*;/ { next } /^\$ORIGIN[[:space:]]+/ { origin = $2 sub(/\.$/, "", origin) next } /^\$TTL[[:space:]]+/ { next } { owner = "" rtype = "" target = "" if (NF >= 3 && ($2 == "A" || $2 == "AAAA")) { owner = $1 rtype = $2 target = $3 } else if (NF >= 4 && $2 == "IN" && ($3 == "A" || $3 == "AAAA")) { owner = $1 rtype = $3 target = $4 } else if (NF >= 4 && ($3 == "A" || $3 == "AAAA")) { owner = $1 rtype = $3 target = $4 } else if (NF >= 5 && $3 == "IN" && ($4 == "A" || $4 == "AAAA")) { owner = $1 rtype = $4 target = $5 } else { next } sub(/\.$/, "", owner) sub(/\.$/, "", target) if (owner == "" || owner == "@" || owner == "*" || owner == zone) { next } fqdn = owner if (index(owner, ".") == 0) { if (origin != "" && origin != ".") { fqdn = owner "." origin } else { fqdn = owner "." zone } } sub(/\.$/, "", fqdn) if (fqdn !~ ("\\." zone "$")) { next } subdomain = fqdn sub("\\." zone "$", "", subdomain) if (subdomain == "" || subdomain == "*" || subdomain ~ /^\*\./) { next } print subdomain, fqdn, target, rtype } ' "$DYNDNS_BIND_ZONE_FILE" rc=$? if [ "$rc" -ne 0 ]; then _log "Could not parse zone file '$DYNDNS_BIND_ZONE_FILE'." 4 _log "Leave the function '_zone_file_records_get()'..." 1 return 1 fi _log "Leave the function '_zone_file_records_get()'..." 1 return 0 } #=> _zone_file_records_get() # SDuesterhaupt: 2026-06-19 - Compare database records with zone-file records # # Input files: # $1 = db records file (subdomainhostnameiprecord_type) # $2 = zone records file (subdomainhostnameiprecord_type) # # +----------+-------------------+-----------+-----------+ # | username | hostname | IP | IPv6_flag | # +----------+-------------------+-----------+-----------+ # | sub1 | sub1.example24.de | 127.0.0.1 | 0 | # | sub2 | sub2.example24.de | 127.0.0.2 | 0 | # | sub3 | sub3.example24.de | ::1 | 1 | # +----------+-------------------+-----------+-----------+ # # Output format: # delownertype # addownerttltypetarget # # Returns: # 0 on success # 1 on failure _zone_updates_build() { _log "Enter the function '_zone_updates_build()'..." 1 db_file=$1 zone_file=$2 if [ ! -f "$db_file" ] || [ ! -f "$zone_file" ]; then _log "Temporary input files for zone update build are missing." 4 _log "Leave the function '_zone_updates_build()'..." 1 return 1 fi tab=$(printf '\t') # 1) Obtain hosts from DB: update or create new while IFS="$tab" read -r db_sub db_host db_ip db_type; do [ -n "$db_sub" ] || continue # passendes RRset im Zonenfile suchen (nach Host + Typ) zone_match=$( awk -F "$tab" -v host="$db_host" -v rtype="$db_type" ' $2 == host && $4 == rtype { print $0; exit } ' "$zone_file" ) if [ -n "$zone_match" ]; then zone_old_ip=$(printf '%s\n' "$zone_match" | awk -F "$tab" '{print $3}') _log "The subdomain '$db_sub' already exists in the zone file." 2 if [ "$db_ip" != "$zone_old_ip" ]; then _log "The IP of the hostname '$db_host' does need to be updated: ZONE '$zone_old_ip' != DB '$db_ip'." 2 # Alte RRsets für diesen Host bereinigen (A, AAAA, CNAME) printf 'del\t%s\tA\n' "$db_host" printf 'del\t%s\tAAAA\n' "$db_host" printf 'del\t*.%s\tCNAME\n' "$db_host" # Neue Records setzen (A/AAAA + Wildcard-CNAME) printf 'add\t%s\t%s\t%s\t%s\n' \ "$db_host" "$DYNDNS_BIND_ZONE_TTL" "$db_type" "$db_ip" printf 'add\t*.%s\t%s\tCNAME\t%s\n' \ "$db_host" "$DYNDNS_BIND_ZONE_TTL" "$db_host" else _log "The IP of the hostname '$db_host' does not need to be updated: ZONE '$zone_old_ip' = DB '$db_ip'." 2 fi else _log "Try to add the subdomain '$db_sub' to the zone file. ..." 2 # Neuer Host: A/AAAA + Wildcard-CNAME printf 'add\t%s\t%s\t%s\t%s\n' \ "$db_host" "$DYNDNS_BIND_ZONE_TTL" "$db_type" "$db_ip" printf 'add\t*.%s\t%s\tCNAME\t%s\n' \ "$db_host" "$DYNDNS_BIND_ZONE_TTL" "$db_host" fi done < "$db_file" # 2) Hosts aus Zone: verwaiste Records löschen (kein DB-Eintrag mehr) # Wir löschen wieder das komplette RRset (A, AAAA, CNAME) wie im alten Skript. while IFS="$tab" read -r z_sub z_host z_ip z_type; do [ -n "$z_sub" ] || continue db_match=$( awk -F "$tab" -v host="$z_host" -v rtype="$z_type" ' $2 == host && $4 == rtype { print $0; exit } ' "$db_file" ) if [ -z "$db_match" ]; then _log "Try to delete the subdomain '$z_sub' from the zone file. ..." 2 printf 'del\t%s\tA\n' "$z_host" printf 'del\t%s\tAAAA\n' "$z_host" printf 'del\t*.%s\tCNAME\n' "$z_host" fi done < "$zone_file" _log "Leave the function '_zone_updates_build()'..." 1 return 0 } #=> _zone_updates_build() # SDuesterhaupt: 2026-06-19 - Apply dynamic DNS updates using nsupdate # # @param1: update file generated by _zone_updates_build # # Returns: # 0 on success # 1 on failure _zone_updates_apply() { _log "Enter the function '_zone_updates_apply()'..." 1 updates_file=$1 nsupdate_file=$(dyndns_mktemp) || { _log "Could not create temporary nsupdate file." 4 _log "Leave the function '_zone_updates_apply()'..." 1 return 1 } _log "Check the zone '$DYNDNS_BIND_ZONE' for DNSSEC capabilities..." 1 if [ -f "$DYNDNS_BIND_ZONE_FILE_SIGNED" ]; then _log "The zone '$DYNDNS_BIND_ZONE' uses DNSSEC..." 1 zone_uses_dnssec=1 else _log "The zone '$DYNDNS_BIND_ZONE' does not use DNSSEC..." 1 zone_uses_dnssec=0 fi { printf 'debug\n' printf 'server %s\n' "$DYNDNS_BIND_SERVER" printf 'zone %s.\n' "$DYNDNS_BIND_ZONE" if [ "$zone_uses_dnssec" -eq 1 ]; then printf '; Move %s from secure to insecure temporarily\n' "$DYNDNS_BIND_ZONE_FILE" printf 'update delete %s. DNSKEY\n' "$DYNDNS_BIND_ZONE" printf 'send\n' fi awk -F '\t' -v zone_file="$DYNDNS_BIND_ZONE_FILE" ' $1 == "del" { del_owner[++del_count] = $2 del_type[del_count] = $3 next } $1 == "add" { add_owner[++add_count] = $2 add_ttl[add_count] = $3 add_type[add_count] = $4 add_target[add_count] = $5 next } END { now = strftime("%d.%m.%Y %H:%M:%S") if (del_count > 0 || add_count > 0) { printf "; Dynamic update of the zone %s - %s\n", zone_file, now } if (del_count > 0) { printf "; Delete the zone records which need an update\n" for (i = 1; i <= del_count; i++) { # printf "prereq yxrrset %s %s\n", del_owner[i], del_type[i] printf "update delete %s %s\n", del_owner[i], del_type[i] } printf "; send\n\n" } if (add_count > 0) { printf "; Add the updated zone records\n" for (i = 1; i <= add_count; i++) { # printf "prereq nxrrset %s %s\n", add_owner[i], add_type[i] printf "update add %s %s %s %s\n", add_owner[i], add_ttl[i], add_type[i], add_target[i] } printf "; send\n" } if (del_count > 0 || add_count > 0) { printf "\nsend\n" } } ' "$updates_file" } > "$nsupdate_file" || { rm -f "$nsupdate_file" _log "Could not build nsupdate instruction file." 4 _log "Leave the function '_zone_updates_apply()'..." 1 return 1 } cp "$nsupdate_file" "$DYNDNS_LOG_UPDATE_FILE" 2>/dev/null || true if ! nsupdate -k "$DYNDNS_BIND_ZONE_KEY" "$nsupdate_file" >/dev/null 2>&1; then rm -f "$nsupdate_file" _log "Zone updates could not be applied." 4 _log "Leave the function '_zone_updates_apply()'..." 1 return 1 fi #nsupdate_output=$(nsupdate -v -k "$DYNDNS_BIND_ZONE_KEY" "$nsupdate_file" 2>&1) #nsupdate_rc=$? #if [ "$nsupdate_rc" -ne 0 ]; then # printf '%s\n' "$nsupdate_output" >>"$DYNDNS_LOG_FILE" # cp "$nsupdate_file" "$DYNDNS_LOG_UPDATE_FILE.failed" 2>/dev/null || true # rm -f "$nsupdate_file" # _log "Zone updates could not be applied." 4 # _log "Leave the function '_zone_updates_apply()'..." 1 # return 1 #fi rm -f "$nsupdate_file" _log "Zone updates were applied successfully." 2 if command -v rndc >/dev/null 2>&1; then if rndc -s "$DYNDNS_BIND_SERVER" -k "$DYNDNS_BIND_ZONE_KEY" freeze "$DYNDNS_BIND_ZONE" >/dev/null 2>&1; then _log "The zone '$DYNDNS_BIND_ZONE' was frozen." 2 if rndc -s "$DYNDNS_BIND_SERVER" -k "$DYNDNS_BIND_ZONE_KEY" thaw "$DYNDNS_BIND_ZONE" >/dev/null 2>&1; then _log "The zone '$DYNDNS_BIND_ZONE' was reloaded." 2 else _log "Couldn't reload the zone '$DYNDNS_BIND_ZONE'." 4 _log "Leave the function '_zone_updates_apply()'..." 1 return 1 fi else _log "Couldn't freeze the zone '$DYNDNS_BIND_ZONE'." 4 _log "Leave the function '_zone_updates_apply()'..." 1 return 1 fi fi _log "Leave the function '_zone_updates_apply()'..." 1 return 0 } #=> _zone_updates_apply() # SDuesterhaupt: 2026-06-18 - Update the dynamic DNS zone based on database content # # Normalized record format: # subdomainhostnameiprecord_type # # Returns: # 0 on success # 1 on failure _update_zone_dynamic() { _log "Enter the function '_update_zone_dynamic()'..." 1 db_file=$(dyndns_mktemp) || { _log "Could not create temporary file for database records." 4 _log "Leave the function '_update_zone_dynamic()'..." 1 return 1 } zone_file=$(dyndns_mktemp) || { _log "Could not create temporary file for zone records." 4 _log "Leave the function '_update_zone_dynamic()'..." 1 return 1 } updates_file=$(dyndns_mktemp) || { _log "Could not create temporary file for zone updates." 4 _log "Leave the function '_update_zone_dynamic()'..." 1 return 1 } _zone_db_records_get > "$db_file" || { _log "Failed to obtain current DNS records from database." 4 _log "Leave the function '_update_zone_dynamic()'..." 1 return 1 } record_count=$(wc -l < "$db_file" 2>/dev/null) _log "Database zone records were written to '$db_file' ($record_count entries)." 1 _zone_file_records_get > "$zone_file" || { _log "Failed to obtain current DNS records from zone file." 4 _log "Leave the function '_update_zone_dynamic()'..." 1 return 1 } record_count=$(wc -l < "$zone_file" 2>/dev/null) _log "Zone file records were written to '$zone_file' ($record_count entries)." 1 # DEBUG: dump temp files #cp "$db_file" "$DYNDNS_LOG_DIR/db_records_${DYNDNS_BIND_ZONE}.tmp" #cp "$zone_file" "$DYNDNS_LOG_DIR/zone_records_${DYNDNS_BIND_ZONE}.tmp" _zone_updates_build "$db_file" "$zone_file" > "$updates_file" || { _log "Failed to generate DNS update instructions." 4 _log "Leave the function '_update_zone_dynamic()'..." 1 return 1 } record_count=$(wc -l < "$updates_file" 2>/dev/null) _log "DNS update instructions were written to '$updates_file' ($record_count entries)." 1 # DEBUG: dump temp files #cp "$updates_file" "$DYNDNS_LOG_DIR/updates_${DYNDNS_BIND_ZONE}.tmp" if [ ! -s "$updates_file" ]; then _log "No DNS changes are required for zone '$DYNDNS_BIND_ZONE'." 2 _log "Leave the function '_update_zone_dynamic()'..." 1 return 0 fi _log "Generated DNS update instructions:" 1 while IFS= read -r line; do _log "$line" 1 done < "$updates_file" _zone_updates_apply "$updates_file" || { _log "Applying DNS updates failed." 4 _log "Leave the function '_update_zone_dynamic()'..." 1 return 1 } _log "Leave the function '_update_zone_dynamic()'..." 1 return 0 } #=> _update_zone_dynamic() ######################################## # Invocation entry point: NL=' ' # Be secure with a restrictive umask [ -z "$DYNDNS_NO_UMASK" ] && umask 077 # Ignore some env vars DYNDNS_PASSIN= DYNDNS_PASSOUT= # Parse options while :; do opt=${1%%=*} val=${1#*=} empty_ok="" case "$opt" in --email|-E) export DYNDNS_MEMBER_EMAIL="$val" ;; --hostname|-H) export DYNDNS_MEMBER_HOSTNAME="$val" ;; --key|-K) export DYNDNS_BIND_ZONE_KEY="$val" ;; --pass|-P) export DYNDNS_MEMBER_PASS="$val" ;; --vars|-V) export DYNDNS_VARS_FILE="$val" ;; --zone|-Z) export DYNDNS_BIND_ZONE="$val" ;; *) break ;; esac if [ -z "$empty_ok" ] && { [ "$val" = "$1" ] || [ -z "$val" ]; }; then _die "Missing value to option: $opt" 1 fi shift done # SDuesterhaupt: 2019-07-16 - Intelligent env-var detection and auto-loading _vars_setup # SDuesterhaupt: 2019-07-28 - Create the log directory _init_log # SDuesterhaupt: 2019-07-16 - Register cleanup on EXIT trap "cleanup" EXIT # SDuesterhaupt: 2019-07-16 - When SIGHUP, SIGINT, SIGQUIT, SIGABRT and SIGTERM, # explicitly exit to signal EXIT (non-bash shells) trap "exit 1" 1 trap "exit 2" 2 trap "exit 3" 3 trap "exit 6" 6 trap "exit 15" 15 # SDuesterhaupt: 2019-07-16 - determine how we were called, then hand off to the function responsible cmd=$1 [ -n "$1" ] && shift # scrape off command case "$cmd" in ""|help|-h|--help|--usage) _cmd_help "$1" exit 0 ;; add-hostname|check-hostname|remove-hostname|update-zone) ;; *) _die "Unknown command '$cmd'. Run without commands for usage help." 1 ;; esac # SDuesterhaupt: 2019-07-16 - Verify runtime prerequisites _verify_runtime_init # SDuesterhaupt: 2019-07-30 - Check the DB connection _init_DBconnection #_log "Command dispatcher sees cmd='$cmd'" 1 #printf '%s\n' "TRACE: dispatch cmd='$cmd'" >&2 case "$cmd" in add-hostname) _add_hostname "$@" exit $? ;; check-hostname) _check_hostname "$@" exit $? ;; remove-hostname) _remove_hostname "$@" exit $? ;; update-zone) _update_zone_dynamic "$@" exit $? ;; esac # vim: ft=sh nu ai sw=8 ts=8 noet