#!/bin/bash
# Copyright 2014-2020, 2023 eomanis
#
# This file is part of yabddnsd.
#
# yabddnsd is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# yabddnsd 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 yabddnsd. If not, see <http://www.gnu.org/licenses/>.
# TODO Sleeping time: Shorter sleep for host IP address checking, longer
# sleep between updates
# That way we detect a host IP address change quickly, but we do
# not spam the dynamic DNS service provider with update requests
# either
# Take however into account that the "Url@@url" host IP address
# detection methods also contact external servers; do not spam
# those servers
# TODO If --update-protocol is not specified make an educated guess
# based on the domain name, for example if the domain name ends
# with ".duckdns.org" then the update protocol is "DuckDns"
# TODO DNS lookups: Use delv instead of dig
# TODO Bash completion
# TODO Assign and parse short (single-character) arguments
# TODO Trap SIGHUP and, if the signal is caught, immediately resume the
# main loop
# Set shell options
set -o pipefail
set -o noclobber
set -o errexit
# Enable the "nounset" shell option only if this is bash>=4.4, which is
# the minimum version required for proper handling of empty arrays
# Coincidentally the "inherit_errexit" option became available starting
# with bash 4.4 as well, so we can conditionally enable it together
# with "nounset"
if test "${BASH_VERSINFO[0]}" != "" \
&& { test "${BASH_VERSINFO[0]}" -ge 5 \
|| { test "${BASH_VERSINFO[0]}" -ge 4 \
&& test "${BASH_VERSINFO[1]}" -ge 4; \
}; \
}; then
set -o nounset
shopt -qs inherit_errexit
else
# printMsgWarning is not declared yet so we cannot use it here
echo " WARN Bash version is less than 4.4; this is untested, your mileage may vary;" \
"not enabling shell options \"nounset\" (buggy with arrays) and \"inherit_errexit\" (unsupported)" >&2
fi
# Semantic versioning
declare -r versionMajor=0
declare -r versionMinor=11
declare -r versionPatch=0
declare -r versionLabel=""
getVersion () {
echo -n "${versionMajor}.${versionMinor}.${versionPatch}"
test "$versionLabel" != "" && echo "-$versionLabel" || echo ""
}
# Buffers for composing multi-part log messages
messageBufferError=()
messageBufferWarning=()
messageBufferInfo=()
messageBufferDebug=()
# printMsg levelPrefix message
#
# Prints the given message to STDERR, prefixed with the given level prefix
printMsg () {
local prefix=$1; shift
if test $# -gt 0; then echo "$prefix $*" >&2; else true; fi
}
# appendMsgError partialMessage
#
# Appends the given partial message to the message buffer for ERROR messages
appendMsgError () {
messageBufferError+=("$@")
}
# printMsgError message
#
# Prints the given message to STDERR, prefixed with "ERROR "
printMsgError () {
printMsg "ERROR " "${messageBufferError[@]}" "$@"; messageBufferError=()
}
# appendMsgWarning partialMessage
#
# Appends the given partial message to the message buffer for WARN messages
appendMsgWarning () {
messageBufferWarning+=("$@")
}
# printMsgWarning message
#
# Prints the given message to STDERR, prefixed with " WARN "
printMsgWarning () {
printMsg " WARN " "${messageBufferWarning[@]}" "$@"; messageBufferWarning=()
}
# appendMsgInfo partialMessage
#
# Appends the given partial message to the message buffer for INFO messages
appendMsgInfo () {
messageBufferInfo+=("$@")
}
# printMsgInfo message
#
# Prints the given message to STDERR, prefixed with " INFO "
printMsgInfo () {
printMsg " INFO " "${messageBufferInfo[@]}" "$@"; messageBufferInfo=()
}
# appendMsgDebug partialMessage
#
# If $verbose is "true", appends the given partial message to the message
# buffer for DEBUG messages
appendMsgDebug () {
test "$verbose" = 'true' || return 0
messageBufferDebug+=("$@")
}
# printMsgDebug message
#
# If $verbose is "true", prints the given message to STDERR, prefixed with
# "DEBUG "
printMsgDebug () {
test "$verbose" = 'true' || return 0
printMsg "DEBUG " "${messageBufferDebug[@]}" "$@"; messageBufferDebug=()
}
# Prints some concise usage information to STDOUT
printUsageInfo () {
echo -n \
"Usage:
yabddnsd
[--domain-name domainName]
[--auth-token-ipv4 authenticationTokenForIPv4]
[--auth-token-ipv6 authenticationTokenForIPv6]
[--config-file sourcedConfigurationFile]
[--detect-public-addr-ipv4 method[@@argument][,method[@@argument]]...]
[--detect-public-addr-ipv6 method[@@argument][,method[@@argument]]...]
[--dns-server dnsServer]
[--dns-server-ipv4 dnsServerForIPv4]
[--dns-server-ipv6 dnsServerForIPv6]
[--one-shot]
[--sleep-time sleepingTimeBetweenIterations]
[--update-protocol updateProtocol]
[--verbose]
yabddnsd --help
yabddnsd --version
yabddnsd --list-functions
yabddnsd [other options]...
--call-function function [functionArguments...]
"
}
# Prints a short help message / summary to STDOUT
printHelpMessage () {
echo -n "yabddnsd "; getVersion
echo "Yet another bash dynamic DNS daemon"
echo ""
printUsageInfo
echo ""
echo -n \
"Periodically checks which IP addresses are listed in the given domain
name's DNS record, and which public IP address this system has.
If the system's public IP address isn't among the DNS record's IP
addresses, the DNS record is updated to the system's public IP address.
For more information see manual page yabddnsd(8).
"
}
# getObfuscatedAuthToken authToken
#
# Obfuscates the given authentication token
getObfuscatedAuthToken () {
local authToken="$1"; shift
if test ${#authToken} -gt 8; then
echo -n "$authToken" | sed -re 's/^(.*)....$/\1/;s/./*/g'
echo "$authToken" | sed -re 's/^.*(....)$/\1/'
else
# Too short, showing half or more of the token seems like a bad
# idea
# Also the obfuscation code does not handle strings of less than
# 4 characters properly anyway
# Therefore just hide the entire string
echo "$authToken" | sed -re 's/./*/g'
fi
}
# isStringInRange lowerBorderIncluding upperBorderExcluding string
#
# Returns with code 0 if the given string is lexicographically
# lowerBorderIncluding <= string < upperBorderExcluding
isStringInRange () {
local lowerBorderIncluding="$1"; shift
local upperBorderExcluding="$1"; shift
local string="$1"; shift
test "$string" '<' "$lowerBorderIncluding" && return 1
test "$string" = "$upperBorderExcluding" && return 1
test "$string" '>' "$upperBorderExcluding" && return 1
return 0
}
# containsExactLine lineToFind
#
# Walks a newline-separated list of strings from STDIN and returns
# with code 0 if any of them literally match lineToFind
containsExactLine () {
local lineToFind="$1"; shift
local maxCharsInclLineFeed=$(( ${#lineToFind} + 1 ))
local line
# The
# || test "$line" != ""
# condition ("or if $line is not empty") ensures that the final text
# line is processed even if it does not end with a line feed
# For read, the final line terminating without a line feed is an
# "unexpected end-of-stream" error condition
# If this occurs it does assign what has been read so far to $line,
# but then it terminates with a non-zero return code, and that
# prevents $line from being processed by the loop body unless a step
# like that is taken
while read -rsn "$maxCharsInclLineFeed" line || test "$line" != ""; do
if test "$line" = "$lineToFind"; then
return 0
fi
done
return 1
}
# joinLines prefix suffix separator [appendFinalNewline]
#
# Walks a newline-separated list of Strings from STDIN and prints
# them to STDOUT, separated (but not terminated) by separator
# The whole printed text is prefixed with prefix and suffixed with
# suffix
# If appendFinalNewline is given and is the string "true", then a final
# trailing newline will be written to STDOUT after all lines have been
# written
joinLines () {
local prefix="$1"; shift
local suffix="$1"; shift
local separator="$1"; shift
local appendFinalNewline=false
if test $# -gt 0; then
test "true" = "$1" && appendFinalNewline=true
shift
fi
local subsequentIteration=false
local previousLine
local line
while read -rs line || test "$line" != ""; do
if $subsequentIteration; then
echo -n "$previousLine"
echo -n "$separator"
else
echo -n "$prefix"
fi
previousLine="$line"
subsequentIteration=true
done
$subsequentIteration && echo -n "$previousLine"
$subsequentIteration && echo -n "$suffix"
$appendFinalNewline && echo ""
return 0
}
# isBetterLifetime lifetimeToTest lifetimeReference
#
# The arguments must be empty, an integer, or the string "forever"
isBetterLifetime () {
local lifetimeToTest="$1"; shift
local lifetimeReference="$1"; shift
if ! test "" = "$lifetimeToTest" && ! test "forever" = "$lifetimeToTest" && ! echo "$lifetimeToTest" | grep -E '^[0-9]+$' &> /dev/null; then
# The lifetime to test is neither empty, nor "forever", nor an
# integer, therefore it is considered invalid to begin with
return 1
fi
if test "" = "$lifetimeToTest"; then
# An empty lifetime always loses
false
elif test "forever" = "$lifetimeToTest"; then
# A lifetime of "forever" always wins, except against another
# lifetime of "forever"
test "forever" != "$lifetimeReference"
else
# Integer lifetime
# Always wins against an empty reference lifetime
test "" = "$lifetimeReference" && return 0
# Always loses against a reference lifetime of "forever"
test "forever" = "$lifetimeReference" && return 1
# Wins against a lesser integer reference lifetime
test "$lifetimeToTest" -gt "$lifetimeReference"
fi
}
# splitBySubstring string substring
#
# Splits the given string at the first occurrence of the given
# substring and returns both parts in a newline-separated list
# The substring at which the string is split is removed
# If the substring does not occur in the given string, the string is
# returned as-is
splitBySubstring () {
local string="$1"; shift
local substring="$1"; shift
local stringLength="${#string}"
local substringLength="${#substring}"
local -i index=0
local testSubstring
local -i startIndexRemainder
while test "$index" -lt "$stringLength"; do
testSubstring="${string:$index:$substringLength}"
if test "$testSubstring" = "$substring"; then
break
fi
index=$(( index + 1 ))
done
if test "$index" -lt "$stringLength"; then
# Substring was found at $index
startIndexRemainder=$(( index + substringLength ))
echo "${string:0:$index}"
echo "${string:$startIndexRemainder}"
else
# Substring was not found
echo "$string"
fi
}
# Returns the paths to all existing valid system-level configuration
# files, in ascending order of priority
#
# The paths are returned in a newline-separated list
getConfigurationFilesSystem () {
getConfigurationFiles "/usr/lib" "/etc" "/run"
}
# Returns the paths to all existing valid user-level configuration
# files, in ascending order of priority
#
# The paths are returned in a newline-separated list
getConfigurationFilesUser () {
getConfigurationFiles ~/".config"
}
# getConfigurationFiles baseDir [otherBaseDir...]
#
# Returns the paths to all existing valid configuration files, in
# ascending order of priority, in the given directories
#
# The paths are returned in a newline-separated list
# The directories must be given without trailing slash
getConfigurationFiles () {
local suffix=".conf"
local pathPrefix
local configDir
local configFile
local baseDir
while test $# -gt 0; do
baseDir="$1"; shift
pathPrefix="${baseDir}/yabddnsd/yabddnsd"
configDir="${pathPrefix}.d"
configFile="${pathPrefix}$suffix"
if test -d "$configDir"; then
find "$configDir" -mindepth 1 -maxdepth 1 -type f -name "*$suffix" -print | sort
fi
if test -f "$configFile"; then
echo "$configFile"
fi
done
}
# isMethodWithArgument string
isMethodWithArgument () {
echo "$1" | grep -E '^.+@@.+$' &> /dev/null
}
# getMethodName methodName@@argument
getMethodName () {
local methodAndArgument="$1"; shift
# Suppress spurious confusing messages like
# "/usr/bin/yabddnsd: line 219: echo: write error: Broken pipe"
# caused by "head" terminating before all data has been piped
# Also, ignore bad exit code 141 caused by SIGPIPE, and also exit
# code 1 which is seen in certain circumstances (such as when this
# application is run from a procd init script on OpenWRT)
splitBySubstring "$methodAndArgument" '@@' 2> /dev/null \
| head -q -n 1 || isWhitelistedExitCode $? 1 141
}
# getMethodArgument methodName@@argument
getMethodArgument () {
local methodAndArgument="$1"; shift
splitBySubstring "$methodAndArgument" '@@' | tail -n +2
}
# isFunction command
#
# Returns with code 0 if the given command refers to a function
# (and not, for example, to a shell builtin or an executable)
isFunction () {
local command="$1"; shift
local commandType
! commandType="$(type -t "$command")" && return 1
! test "function" = "$commandType" && return 1
return 0
}
# isWhitelistedExitCode exitCode [whitelistedExitCode...]
#
# Returns with code 0 if exitCode is any of the given whitelisted
# possibilities
# If it isn't, returns with exitCode
isWhitelistedExitCode () {
local exitCode="$1"; shift
while test $# -ge 1; do
test "$exitCode" -eq "$1" && return 0
shift
done
return "$exitCode"
}
# getIpv4AddrAsHexString ipv4Addr
#
# Prints the given IPv4 address as an 8-character upper case hexadecimal
# string, such as "0B2D062F" for the IPv4 address "11.45.6.47"
#
# Prints nothing and returns with code 1 if the given string is not
# an IPv4 address
getIpv4AddrAsHexString () {
local ipv4Addr="$1"; shift
local -i -a octets
local -a octetsHex=()
local -i index
local currentWord
local result
# Input must not be an empty string, must be exactly 1 line of text
{ test "" != "$ipv4Addr" && test "$(echo "$ipv4Addr" | wc -l)" -eq 1; } || return 1
# Input must have an IPv4-like structure
{ echo "$ipv4Addr" | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' &> /dev/null; } || return 1
# Convert the IPv4 address to a hexadecimal string, handling each
# octet individually
index=0
result=""
while read -rsn 4 -d '.' currentWord; do
test $index -lt 4 || return 1
# Store the current octet into the integer array
octets[index]="$currentWord"
# Crudely sanity-check the octet
{ test "${octets[$index]}" != "" && test "${octets[$index]}" -ge 0 && test "${octets[$index]}" -le 255; } || return 1
# Convert the octet to a 2-character hexadecimal representation
octetsHex[index]="$(echo "obase=16; ${octets[$index]}" | bc | sed -re 's/^(.)$/0\1/')"
# Append the octet's 2-char hexadecimal representation to the
# hexadecimal string of the IPv4 address
result="${result}${octetsHex[$index]}"
index=$(( index + 1 ))
done < <(echo "${ipv4Addr}.")
#printMsgDebug "8-character hexadecimal representation of IPv4 address \"$ipv4Addr\" is \"$result\""
echo "$result"
}
# isPublicIpv4Addr ipv4Addr
#
# Returns with code 0 if ipv4Addr is a public IPv4 address
isPublicIpv4Addr () {
local ipv4Addr="$1"; shift
local ipv4AddrHex
# We opt for string comparisons instead of integer arithmetic
# On 32-bit systems bash might use a SInt32 for integers which might
# overflow for an IPv4 address (we would need an UInt32 at least)
# Also whatever bash uses, it will most likely be insufficient to
# hold an IPv6 integer, and it would be nice to use the same kind
# of procedure for IPv4 and IPv6
ipv4AddrHex="$(getIpv4AddrAsHexString "$ipv4Addr")" || return 1
# Current network, 0.0.0.0/8
isStringInRange "00000000" \
"01000000" "$ipv4AddrHex" && return 1
# Former class A private network, 10.0.0.0/8
isStringInRange "0A000000" \
"0B000000" "$ipv4AddrHex" && return 1
# Shared address space (carrier-grade NAT), 100.64.0.0/10
isStringInRange "64400000" \
"64800000" "$ipv4AddrHex" && return 1
# Loopback addresses, 127.0.0.0/8
isStringInRange "7F000000" \
"80000000" "$ipv4AddrHex" && return 1
# Link-local addresses, 169.254.0.0/16
isStringInRange "A9FE0000" \
"A9FF0000" "$ipv4AddrHex" && return 1
# Former class B private network, 172.16.0.0/12
isStringInRange "AC100000" \
"AC200000" "$ipv4AddrHex" && return 1
# IETF Protocol assignments, 192.0.0.0/24
isStringInRange "C0000000" \
"C0000100" "$ipv4AddrHex" && return 1
# TEST-NET-1, 192.0.2.0/24
isStringInRange "C0000200" \
"C0000300" "$ipv4AddrHex" && return 1
# Former IPv6 to IPv4 relay, 192.88.99.0/24
isStringInRange "C0586300" \
"C0586400" "$ipv4AddrHex" && return 1
# Former class C private network, 192.168.0.0/16
isStringInRange "C0A80000" \
"C0A90000" "$ipv4AddrHex" && return 1
# Inter-network communication testing, 198.18.0.0/15
isStringInRange "C6120000" \
"C6140000" "$ipv4AddrHex" && return 1
# TEST-NET-2, 198.51.100.0/24
isStringInRange "C6336400" \
"C6336500" "$ipv4AddrHex" && return 1
# TEST-NET-3, 203.0.113.0/24
isStringInRange "CB007100" \
"CB007200" "$ipv4AddrHex" && return 1
# IP multicast (former class D network), 224.0.0.0/4
isStringInRange "E0000000" \
"F0000000" "$ipv4AddrHex" && return 1
# Reserved for future use (former class E network), 240.0.0.0/4
# excepting 255.255.255.255
isStringInRange "F0000000" \
"FFFFFFFF" "$ipv4AddrHex" && return 1
# Limited broadcast destination address, 255.255.255.255/32
test "FFFFFFFF" = "$ipv4AddrHex" && return 1
return 0
}
# getPublicIpv4AddrFromStream
#
# Scans STDIN for a text line containing a single public IPv4 address
# and, if a single such text line occurred, writes that IPv4 address to
# STDOUT
# If the stream did not contain such a text line, or contained multiple
# such text lines, writes nothing and returns with code 1
# The input stream may for example be a plain-text file or an (X)HTML
# document
getPublicIpv4AddrFromStream () {
local line
local candidateAddress
local result
while read -rsn 500 line || test "$line" != ""; do
# Continue if the line does not contain an IPv4 address
echo "$line" | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' &> /dev/null || continue
# The line contains an IPv4 address: Extract it
if echo "$line" | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' &> /dev/null; then
# The line is already an IPv4 address, no extraction
# required
candidateAddress="$line"
else
# There is an IPv4 address embedded somewhere in the text
# line (maybe between some HTML tags), and it needs to be
# extracted
candidateAddress="$(echo "$line" | sed -re 's/^.*[^0-9.]([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*$/\1/')" || return 1
fi
# Continue if the extraction failed
test "" = "$candidateAddress" && continue
# Continue if the IPv4 address is not a public address
! isPublicIpv4Addr "$candidateAddress" && continue
# Return with code 1 if this is the 2nd line to contain a public
# IPv4 address
! test -z ${result+x} && return 1
# Store the current public IPv4 address
result="$candidateAddress"
done
# Return with code 1 if none of the lines contained a public IPv4
# address
test -z ${result+x} && return 1
printMsgDebug "Public IPv4 address read from stream: \"${result}\""
echo "$result"
}
# getIpv4AddrOfThisHostDefault
#
# Determines this host's public IPv4 address using the configured
# method(s)
getIpv4AddrOfThisHostDefault () {
getIpv4AddrOfThisHost "${detectPublicAddrIpv4[@]}"
}
# getIpv4AddrOfThisHost [methodString...]
#
# Determines this host's public IPv4 address using the given method(s)
getIpv4AddrOfThisHost () {
local methodString
local methodName
local -a methodArgumentIfSet
local functionName
local result
while test $# -gt 0; do
methodString="$1"; shift
if isMethodWithArgument "$methodString"; then
methodName="$(getMethodName "$methodString")"
methodArgumentIfSet=( "$(getMethodArgument "$methodString")" )
else
methodName="$methodString"
methodArgumentIfSet=()
fi
functionName="getIpv4AddrOfThisHostFrom$methodName"
printMsgDebug "Getting this host's public IPv4 address from function \"$functionName\""
if result="$("$functionName" "${methodArgumentIfSet[@]}")" && test "$result" != ""; then
printMsgDebug "Public IPv4 address is \"$result\""
echo "$result"
return 0
fi
done
return 1
}
# getIpv4AddrOfThisHostFromFile path
#
# Extracts this host's public IPv4 address from the given text or
# (X)HTML file
getIpv4AddrOfThisHostFromFile () {
if test $# -eq 0; then
printMsgWarning "Failed to read this host's IPv4 address from file: No file path given as argument"
return 1
fi
getPublicIpv4AddrFromStream < "$1"
}
# getIpv4AddrOfThisHostFromNetDev [networkDevice]
#
# Determines this host's public IPv4 address using
# ip -family inet -oneline addr show [networkDevice]
getIpv4AddrOfThisHostFromNetDev () {
local -a networkDeviceIfSet=()
local currentLine
local currentNetworkDev
local currentAddress
local currentLifetimeSec
local bestAddress
local bestLifetimeSec
# Read the network device argument if present
if test $# -gt 0; then
networkDeviceIfSet=( "$1" ); shift
fi
# Extract and validate the IP addresses from the text lines
while read -rs currentLine; do
currentAddress="$(echo "$currentLine" | sed -re 's/^.+ inet ([0-9.]+).*$/\1/')" || continue
currentNetworkDev="$(echo "$currentLine" | sed -re 's/^[^:]+: ([^ ]+) .*$/\1/')" || continue
currentLifetimeSec="$(echo "$currentLine" | sed -re 's/^.+ valid_lft ([^ ]+).*$/\1/;s/sec$//')" || continue
isPublicIpv4Addr "$currentAddress" || continue
printMsgDebug "Public IPv4 address read from network device \"${currentNetworkDev}\": \"${currentAddress}\", valid lifetime (sec): $currentLifetimeSec"
if test -z ${bestAddress+x} || isBetterLifetime "$currentLifetimeSec" "$bestLifetimeSec"; then
# This is the first valid public IP address, or a subsequent
# address that beats the preceding one by valid lifetime
bestAddress="$currentAddress"
bestLifetimeSec="$currentLifetimeSec"
elif test "$currentLifetimeSec" = "$bestLifetimeSec"; then
# This address has the same lifetime as the preceding one:
# Ambiguous result, abort without a result
unset bestAddress
break
fi
done < <(ip -family inet -oneline addr show "${networkDeviceIfSet[@]}" \
| { grep ' scope global' || true; } \
| { grep -v ' deprecated' || true; } \
| { grep -v ' temporary' || true; } )
# Return with an error code if no valid address was found
test -z ${bestAddress+x} && return 1
# Write the single valid result to STDOUT
echo "$bestAddress"
}
upnpAvailable=false
if type upnpc &> /dev/null; then
upnpAvailable=true
# Determines his host's public IPv4 address using UPNP
getIpv4AddrOfThisHostFromUpnp () {
local candidateLines
local result
candidateLines="$(upnpc -s | grep -E '^ExternalIPAddress = [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')" || return 1
if test "" = "$candidateLines" || ! test "$(echo "$candidateLines" | wc -l)" -eq 1; then
return 1
fi
result="$(echo "$candidateLines" | sed -re 's/^ExternalIPAddress = ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$/\1/')" || return 1
if test "" = "$result" || ! test "$(echo "$result" | wc -l)" -eq 1 || ! isPublicIpv4Addr "$result"; then
return 1
fi
echo "$result"
}
fi
# getIpv4AddrOfThisHostFromUrl url
#
# Determines this host's public IPv4 address using the given website
# URL
getIpv4AddrOfThisHostFromUrl () {
if test $# -eq 0; then
printMsgWarning "Failed to determine this host's IPv4 address from URL: No URL given as argument"
return 1
fi
wget -q4O - "$1" | getPublicIpv4AddrFromStream
}
# getIpv4AddrsOfDomainDefault
#
# Determines the configured domain name's IPv4 addresses using the
# configured IPv4 DNS server
getIpv4AddrsOfDomainDefault () {
getIpv4AddrsOfDomain "$domainName" "${dnsServerIpv4IfSet[@]}"
}
# getIpv4AddrsOfDomain domainName [dnsServerIpv4]
#
# Determines the given domain name's IPv4 addresses
# The addresses are returned in a newline-separated list
# The list is empty if the domain name does not have any IPv4 addresses
# Returns with a non-zero code if an error occurred while resolving the
# domain name's IPv4 addresses
getIpv4AddrsOfDomain () {
local domainName="$1"; shift
local dnsServerIpv4IfSet=()
local commandType
local result
if test $# -gt 0; then
dnsServerIpv4IfSet=( "$1" ); shift
fi
# Option 1: Ask the custom implementation if available
if isFunction "getIpv4AddrsOfDomainCustom"; then
printMsgDebug "Resolving IPv4 addresses of domain name \"$domainName\" using getIpv4AddrsOfDomainCustom"
if result="$(getIpv4AddrsOfDomainCustom "$domainName" "${dnsServerIpv4IfSet[@]}")"; then
printMsgDebug "IPv4 addresses of domain name \"$domainName\" from getIpv4AddrsOfDomainCustom:" \
"$(echo "$result" | joinLines "" "" ", " false)"
echo "$result"
return 0
fi
fi
# Option 2: Use dig
printMsgDebug "Resolving IPv4 addresses of domain name \"$domainName\" using dig"
if result="$(getIpv4AddrsOfDomainFromDig "$domainName" "${dnsServerIpv4IfSet[@]}")"; then
printMsgDebug "IPv4 addresses of domain name \"$domainName\" from dig:" \
"$(echo "$result" | joinLines "" "" ", " false)"
echo "$result"
return 0
fi
return 1
}
# getIpv4AddrsOfDomainFromDig domainName [dnsServerIpv4]
#
# Determines the given domain name's IPv4 addresses using dig
# See getIpv4AddrsOfDomain for the function contract
getIpv4AddrsOfDomainFromDig () {
local domainName="$1"; shift
local dnsServerIpv4IfSet=()
local digResponse
local -i digExitCode
if test $# -gt 0; then
dnsServerIpv4IfSet=( "@$1" ); shift
fi
digResponse="$(dig "${dnsServerIpv4IfSet[@]}" -t A -q "$domainName" +noall +answer +short)" && digExitCode=$? || digExitCode=$?
echo "$digResponse" | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'
test "$digExitCode" = 0 || return "$digExitCode"
}
# updateIpv4AddrIfRequiredDefault
#
# Retrieves the configured domain name's IPv4 addresses using the
# configured IPv4 DNS server, and determines the current local public
# IPv4 address using the configured IPv4 address detection methods
# If required, updates the domain name's IPv4 address using the
# configured update protocol and IPv4 authentication token
updateIpv4AddrIfRequiredDefault () {
updateIpv4AddrIfRequired "$updateProtocol" "$domainName" "$authTokenIpv4" "${dnsServerIpv4IfSet[@]}"
}
# updateIpv4AddrIfRequired updateProtocol domainName authTokenIpv4 [dnsServerIpv4]
#
# Retrieves the domain name's IPv4 addresses using the given IPv4 DNS
# server and determines the current local public IPv4 address using the
# configured IPv4 address detection methods
# If required, updates the domain name's IPv4 address
updateIpv4AddrIfRequired () {
local updateProtocol="$1"; shift
local domainName="$1"; shift
local authTokenIpv4="$1"; shift
local dnsServerIpv4=""
if test $# -gt 0; then
dnsServerIpv4="$1"; shift
fi
updateAddrsIfRequired "$updateProtocol" "$domainName" "$authTokenIpv4" "$dnsServerIpv4" "" ""
}
# updateIpv4AddrDefault newIpv4Addr
#
# Sets the configured domain name's IPv4 address using the configured
# update protocol and IPv4 authentication token
updateIpv4AddrDefault () {
updateIpv4Addr "$updateProtocol" "$domainName" "$authTokenIpv4" "$1"
}
# updateIpv4Addr updateProtocol domainName authTokenIpv4 newIpv4Addr
#
# Sets the domain name's IPv4 address
updateIpv4Addr () {
local updateProtocol="$1"; shift
local domainName="$1"; shift
local authTokenIpv4="$1"; shift
local newIpv4Addr="$1"; shift
updateAddrs "$updateProtocol" "$domainName" "$authTokenIpv4" "$newIpv4Addr" "" ""
}
# updateIpv4AddrWithDuckDns domainName authTokenIpv4 newIpv4Addr
#
# Sets the domain name's IPv4 address using the DuckDns protocol
# (duckdns.org)
updateIpv4AddrWithDuckDns () {
local domainName="$1"; shift
local authTokenIpv4="$1"; shift
local newIpv4Addr="$1"; shift
local authTokenIpv4Obfuscated
authTokenIpv4Obfuscated="$(getObfuscatedAuthToken "$authTokenIpv4")"
local updateUrlIpv4="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenIpv4}&ip=${newIpv4Addr}"
local updateUrlIpv4Obfuscated="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenIpv4Obfuscated}&ip=${newIpv4Addr}"
local response
local -i wgetExitCode
printMsgDebug "Calling update URL: \"$updateUrlIpv4Obfuscated\""
# Return codes related to "head" closing the pipe before "wget" is
# done writing to it should not be considered an error
# 3 (wget, I/O error)
# 141 (wget, received SIGPIPE)
response="$(wget -qO - "$updateUrlIpv4" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
&& wgetExitCode="$?" || wgetExitCode="$?"
test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
printMsgDebug "Response started with \"$response\""
if ! test "$response" = "OK"; then
printMsgWarning "Unexpected response from DuckDNS dynDNS update API: \"$response\""
return 1
fi
return 0
}
# updateIpv4AddrWithFreeDns domainName authTokenIpv4 newIpv4Addr
#
# Sets the domain name's IPv4 address using either the FreeDnsV1 or
# FreeDnsV2 protocol, depending on the type of IPv4 authentication
# token given
# (freedns.afraid.org)
updateIpv4AddrWithFreeDns () {
local domainName="$1"; shift
local authTokenIpv4="$1"; shift
local newIpv4Addr="$1"; shift
if test "${#authTokenIpv4}" -eq 24; then
updateIpv4AddrWithFreeDnsV2 "$domainName" "$authTokenIpv4" "$newIpv4Addr"
else
updateIpv4AddrWithFreeDnsV1 "$domainName" "$authTokenIpv4" "$newIpv4Addr"
fi
}
# updateIpv4AddrWithFreeDnsV1 domainName authTokenIpv4 newIpv4Addr
#
# Sets the domain name's IPv4 address using the FreeDnsV1 protocol
# (freedns.afraid.org)
updateIpv4AddrWithFreeDnsV1 () {
local domainName="$1"; shift
local authTokenIpv4="$1"; shift
local newIpv4Addr="$1"; shift
local authTokenIpv4Obfuscated
authTokenIpv4Obfuscated="$(getObfuscatedAuthToken "$authTokenIpv4")"
local updateUrlIpv4="${updateUrlBaseFreeDnsV1}${authTokenIpv4}&address=${newIpv4Addr}"
local updateUrlIpv4Obfuscated="${updateUrlBaseFreeDnsV1}${authTokenIpv4Obfuscated}&address=${newIpv4Addr}"
local response
local -i wgetExitCode
printMsgDebug "Calling update URL: \"$updateUrlIpv4Obfuscated\""
response="$(wget -qO - "$updateUrlIpv4" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
&& wgetExitCode="$?" || wgetExitCode="$?"
test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
printMsgDebug "Response started with \"$response\""
if ! echo "$response" | grep -E '^Updated ' &> /dev/null \
&& ! echo "$response" | grep -E '^ERROR: Address [^ ]+ has not changed.$' &> /dev/null; then
printMsgWarning "Unexpected response from FreeDNS-v1 dynDNS update API: \"$response\""
return 1
fi
return 0
}
# updateIpv4AddrWithFreeDnsV2 domainName authTokenIpv4 newIpv4Addr
#
# Sets the domain name's IPv4 address using the FreeDnsV2 protocol
# (sync.afraid.org)
updateIpv4AddrWithFreeDnsV2 () {
local domainName="$1"; shift
local authTokenIpv4="$1"; shift
local newIpv4Addr="$1"; shift
local authTokenIpv4Obfuscated
authTokenIpv4Obfuscated="$(getObfuscatedAuthToken "$authTokenIpv4")"
local updateUrlIpv4="${updateUrlBaseFreeDnsV2Ipv4}${authTokenIpv4}/?ip=${newIpv4Addr}"
local updateUrlIpv4Obfuscated="${updateUrlBaseFreeDnsV2Ipv4}${authTokenIpv4Obfuscated}/?ip=${newIpv4Addr}"
local response
local -i wgetExitCode
printMsgDebug "Calling update URL: \"$updateUrlIpv4Obfuscated\""
response="$(wget -qO - "$updateUrlIpv4" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
&& wgetExitCode="$?" || wgetExitCode="$?"
test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
printMsgDebug "Response started with \"$response\""
if ! echo "$response" | grep -E '^Updated ' &> /dev/null \
&& ! echo "$response" | grep -E '^No IP change detected for ' &> /dev/null; then
printMsgWarning "Unexpected response from FreeDNS-v2 dynDNS update API: \"$response\""
return 1
fi
return 0
}
# getIpv6AddrExpanded ipv6Addr
#
# Prints the given IPv6 address to STDOUT in its "expanded" notation,
# such as "2001:db8:0:0:0:0:0:fe" for the IPv6 address "2001:db8::fe"
#
# Upper case characters will be converted to lower case characters
#
# Prints nothing and returns with code 1 if the given string is not an
# IPv6 address
getIpv6AddrExpanded () {
local ipv6Addr="$1"; shift
local comprSectionLoc
local workingString
local result
local index
# Input must not be an empty string, must be exactly 1 line of text
{ test "" != "$ipv6Addr" && test "$(echo "$ipv6Addr" | wc -l)" -eq 1; } || return 1
# ABCDEF to abcdef
ipv6Addr="$(echo "$ipv6Addr" | tr 'A-F' 'a-f')"
# Return with code 1 if the input contains illegal characters
echo "$ipv6Addr" | grep -E '[^0-9abcdef:]' &> /dev/null && return 1
# Return with code 1 if the input contains ":::"
echo "$ipv6Addr" | grep -E '[:][:][:]' &> /dev/null && return 1
# Return with code 1 if the input contains "::" multiple times
echo "$ipv6Addr" | grep -E '[:][:][^:]+[:][:]' &> /dev/null && return 1
if ! echo "$ipv6Addr" | grep '::' &> /dev/null; then
# Input does not contain "::"
# Return the input if it is already an expanded IPv6 address,
# otherwise return with code 1
echo "$ipv6Addr" | grep -E '^([0-9abcdef]{1,4}[:]){7}[0-9abcdef]{1,4}$' 2> /dev/null || return 1 && return 0
fi
if test '::' = "$ipv6Addr"; then
# Special case "::"
echo '0:0:0:0:0:0:0:0'
return 0
fi
# Determine the :: compressed section's location
if echo "$ipv6Addr" | grep '^::' &> /dev/null; then
# Something like "::2001:db8:fe"
comprSectionLoc=leading
elif echo "$ipv6Addr" | grep '::$' &> /dev/null; then
# Something like "2001:db8:fe::"
comprSectionLoc=trailing
else
# Something like "2001:db8::fe"
comprSectionLoc=enclosed
fi
# Expand the :: compressed section
# Up to 7 iterations, which is just enough to expand a "minimal"
# IPv6 address such as "123::" to "123:0:0:0:0:0:0:0"
# (The special case "::" has already been dealt with and does not
# apply here anymore)
workingString="$ipv6Addr"
for index in 0 1 2 3 4 5 6; do
# Replace the :: section with a zero segment, which might yield
# an expanded IPv6 address
if test leading = $comprSectionLoc; then
result="$(echo "$workingString" | sed -re 's/[:][:]/0:/')"
elif test trailing = $comprSectionLoc; then
result="$(echo "$workingString" | sed -re 's/[:][:]/:0/')"
else
result="$(echo "$workingString" | sed -re 's/[:][:]/:0:/')"
fi
#printMsgDebug "IPv6 expansion intermediate result: \"$result\""
# Return the result if it is now an expanded IPv6 address
echo "$result" | grep -E '^([0-9abcdef]{1,4}[:]){7}[0-9abcdef]{1,4}$' 2> /dev/null && return 0
# The result is not (yet) an expanded IPv6 address:
# Insert a zero segment into the working string and try again
if test leading = $comprSectionLoc; then
workingString="$(echo "$workingString" | sed -re 's/[:][:]/::0:/')"
else
workingString="$(echo "$workingString" | sed -re 's/[:][:]/:0::/')"
fi
done
return 1
}
# getIpv6AddrAsHexString ipv6Addr
#
# Prints the given IPv6 address as a 32-character upper case hexadecimal
# string, such as "20010DB80000000000000000000000FE" for the IPv6
# address "2001:db8::fe"
#
# Prints nothing and returns with code 1 if the given string is not an
# IPv6 address
getIpv6AddrAsHexString () {
local ipv6Addr="$1"; shift
local ipv6AddrExpanded
local -a segmentsHex=()
local -i index
local currentWord
local result
# Expand the IPv6 address (and return with code 1 if that fails)
ipv6AddrExpanded="$(getIpv6AddrExpanded "$ipv6Addr")" || return 1
# Pad the segments with leading zeros, handling each segment
# individually
index=0
result=""
while read -rsn 5 -d ':' currentWord; do
test $index -lt 8 || return 1
# Pad the current segment to 4 characters with leading zeros and
# store it into the hex string array
segmentsHex[index]="$(echo "$currentWord" | sed -re 's/^(...)$/0\1/;s/^(..)$/00\1/;s/^(.)$/000\1/;')"
# Append the padded segment to the result
result="${result}${segmentsHex[$index]}"
index=$(( index + 1 ))
done < <(echo "${ipv6AddrExpanded}:")
# Convert the whole thing to upper case
result="$(echo "$result" | tr 'a-f' 'A-F')"
#printMsgDebug "32-character hexadecimal representation of IPv6 address \"$ipv6Addr\" is \"$result\""
echo "$result"
}
# isPublicIpv6Addr ipv6Addr
#
# Returns with code 0 if ipv6Addr is a public IPv6 address
isPublicIpv6Addr () {
local ipv6Addr="$1"; shift
local ipv6AddrHex
# Convert the IPv6 address to a 32-character upper case hexadecimal
# string (and return with code 1 if that fails)
ipv6AddrHex="$(getIpv6AddrAsHexString "$ipv6Addr")" || return 1
# Unspecified address, ::/128
test "00000000000000000000000000000000" = "$ipv6AddrHex" && return 1
# Local host loopback address, ::1/128
test "00000000000000000000000000000001" = "$ipv6AddrHex" && return 1
# IPv4 mapped addresses, ::ffff:0:0/96
isStringInRange "00000000000000000000FFFF00000000" \
"00000000000000000001000000000000" "$ipv6AddrHex" && return 1
# IPv4 translated addresses, ::ffff:0:0:0/96
isStringInRange "0000000000000000FFFF000000000000" \
"0000000000000000FFFF000100000000" "$ipv6AddrHex" && return 1
# IPv4/IPv6 translation, 64:ff9b::/96
isStringInRange "0064FF9B000000000000000000000000" \
"0064FF9B000000000000000100000000" "$ipv6AddrHex" && return 1
# Discard prefix, 100::/64
isStringInRange "01000000000000000000000000000000" \
"01000000000000010000000000000000" "$ipv6AddrHex" && return 1
# Teredo tunneling, 2001::/32
isStringInRange "20010000000000000000000000000000" \
"20010001000000000000000000000000" "$ipv6AddrHex" && return 1
# ORCHIDv2, 2001:20::/28
isStringInRange "20010020000000000000000000000000" \
"20010030000000000000000000000000" "$ipv6AddrHex" && return 1
# Addresses used for documentation and examples, 2001:db8::/32
isStringInRange "20010DB8000000000000000000000000" \
"20010DB9000000000000000000000000" "$ipv6AddrHex" && return 1
# Deprecated 6to4 addressing scheme, 2002::/16
isStringInRange "20020000000000000000000000000000" \
"20030000000000000000000000000000" "$ipv6AddrHex" && return 1
# Unique local addresses, fc00::/7
isStringInRange "FC000000000000000000000000000000" \
"FE000000000000000000000000000000" "$ipv6AddrHex" && return 1
# Link-local addresses, fe80::/10
isStringInRange "FE800000000000000000000000000000" \
"FEC00000000000000000000000000000" "$ipv6AddrHex" && return 1
# Multicast addresses, ff00::/8
isStringInRange "FF000000000000000000000000000000" \
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" "$ipv6AddrHex" && return 1
test "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" = "$ipv6AddrHex" && return 1
return 0
}
# getPublicIpv6AddrFromStream
#
# Scans STDIN for a text line containing a single public IPv6 address
# and, if a single such text line occurred, writes that IPv6 address to
# STDOUT
# If the stream did not contain such a text line, or contained multiple
# such text lines, writes nothing and returns with code 1
# The input stream may for example be a plain-text file or an (X)HTML
# document
getPublicIpv6AddrFromStream () {
local line
local candidateAddress
local result
while read -rsn 500 line || test "$line" != ""; do
# Continue if the line does not contain an IPv6 address
echo "$line" | grep -E '[0-9abcdef:]+' &> /dev/null || continue
# The line contains an IPv6 address: Extract it
if echo "$line" | grep -E '^[0-9abcdef:]+$' &> /dev/null; then
# The line is already an IPv6 address, no extraction
# required
candidateAddress="$line"
else
# There is an IPv6 address embedded somewhere in the text
# line (maybe between some HTML tags), and it needs to be
# extracted
candidateAddress="$(echo "$line" | sed -re 's/^.*[^0-9.]([0-9abcdef:]+).*$/\1/')" || return 1
fi
# Continue if the extraction failed
test "" = "$candidateAddress" && continue
# Continue if the IPv6 address is not a public address
! isPublicIpv6Addr "$candidateAddress" && continue
# Return with code 1 if this is the 2nd line to contain a public
# IPv6 address
! test -z ${result+x} && return 1
# Store the current public IPv6 address
result="$candidateAddress"
done
# Return with code 1 if none of the lines contained a public IPv6
# address
test -z ${result+x} && return 1
printMsgDebug "Public IPv6 address read from stream: \"${result}\""
echo "$result"
}
# getIpv6AddrOfThisHostDefault
#
# Determines this host's public IPv6 address using the configured
# method(s)
getIpv6AddrOfThisHostDefault () {
getIpv6AddrOfThisHost "${detectPublicAddrIpv6[@]}"
}
# getIpv6AddrOfThisHost [methodString...]
#
# Determines this host's public IPv6 address using the given method(s)
getIpv6AddrOfThisHost () {
local methodString
local methodName
local -a methodArgumentIfSet
local functionName
local result
while test $# -gt 0; do
methodString="$1"; shift
if isMethodWithArgument "$methodString"; then
methodName="$(getMethodName "$methodString")"
methodArgumentIfSet=( "$(getMethodArgument "$methodString")" )
else
methodName="$methodString"
methodArgumentIfSet=()
fi
functionName="getIpv6AddrOfThisHostFrom$methodName"
printMsgDebug "Getting this host's public IPv6 address from function \"$functionName\""
if result="$("$functionName" "${methodArgumentIfSet[@]}")" && test "$result" != ""; then
printMsgDebug "Public IPv6 address is \"$result\""
echo "$result"
return 0
fi
done
return 1
}
# getIpv6AddrOfThisHostFromFile path
#
# Extracts this host's public IPv6 address from the given text or
# (X)HTML file
getIpv6AddrOfThisHostFromFile () {
if test $# -eq 0; then
printMsgWarning "Failed to read this host's IPv6 address from file: No file path given as argument"
return 1
fi
getPublicIpv6AddrFromStream < "$1"
}
# getIpv6AddrOfThisHostFromNetDev [networkDevice]
#
# Determines this host's public IPv6 address using
# ip -family inet6 -oneline addr show [networkDevice]
getIpv6AddrOfThisHostFromNetDev () {
local -a networkDeviceIfSet=()
local currentLine
local currentNetworkDev
local currentAddress
local currentLifetimeSec
local bestAddress
local bestLifetimeSec
# Read the network device argument if present
if test $# -gt 0; then
networkDeviceIfSet=( "$1" ); shift
fi
# Extract and validate the IP addresses from the text lines
while read -rs currentLine; do
currentAddress="$(echo "$currentLine" | sed -re 's/^.+ inet6 ([0-9abcdef:]+).*$/\1/')" || continue
currentNetworkDev="$(echo "$currentLine" | sed -re 's/^[^:]+: ([^ ]+) .*$/\1/')" || continue
currentLifetimeSec="$(echo "$currentLine" | sed -re 's/^.+ valid_lft ([^ ]+).*$/\1/;s/sec$//')" || continue
isPublicIpv6Addr "$currentAddress" || continue
printMsgDebug "Public IPv6 address read from network device \"${currentNetworkDev}\": \"${currentAddress}\", valid lifetime (sec): $currentLifetimeSec"
if test -z ${bestAddress+x} || isBetterLifetime "$currentLifetimeSec" "$bestLifetimeSec"; then
# This is the first valid public IP address, or a subsequent
# address that beats the preceding one by valid lifetime
bestAddress="$currentAddress"
bestLifetimeSec="$currentLifetimeSec"
elif test "$currentLifetimeSec" = "$bestLifetimeSec"; then
# This address has the same lifetime as the preceding one:
# Ambiguous result, abort without a result
unset bestAddress
break
fi
done < <(ip -family inet6 -oneline addr show "${networkDeviceIfSet[@]}" \
| { grep ' scope global' || true; } \
| { grep -v ' deprecated' || true; } \
| { grep -v ' temporary' || true; } )
# Return with an error code if no valid address was found
test -z ${bestAddress+x} && return 1
# Write the single valid result to STDOUT
echo "$bestAddress"
}
# getIpv6AddrOfThisHostFromUrl url
#
# Determines this host's public IPv6 address using the given website
# URL
getIpv6AddrOfThisHostFromUrl () {
if test $# -eq 0; then
printMsgWarning "Failed to determine this host's IPv6 address from URL: No URL given as argument"
return 1
fi
wget -q6O - "$1" | getPublicIpv6AddrFromStream
}
# getIpv6AddrsOfDomain
#
# Determines the configured domain name's IPv6 addresses using the
# configured IPv6 DNS server
getIpv6AddrsOfDomainDefault () {
getIpv6AddrsOfDomain "$domainName" "${dnsServerIpv6IfSet[@]}"
}
# getIpv6AddrsOfDomain domainName [dnsServerIpv6]
#
# Determines the given domain name's IPv6 addresses
# The addresses are returned in a newline-separated list
# The list is empty if the domain name does not have any IPv6 addresses
# Returns with a non-zero code if an error occurred while resolving the
# domain name's IPv6 addresses
getIpv6AddrsOfDomain () {
local domainName="$1"; shift
local dnsServerIpv6IfSet=()
local commandType
local result
if test $# -gt 0; then
dnsServerIpv6IfSet=( "$1" ); shift
fi
# Option 1: Ask the custom implementation if available
if isFunction "getIpv6AddrsOfDomainCustom"; then
printMsgDebug "Resolving IPv6 addresses of domain name \"$domainName\" using getIpv6AddrsOfDomainCustom"
if result="$(getIpv6AddrsOfDomainCustom "$domainName" "${dnsServerIpv6IfSet[@]}")"; then
printMsgDebug "IPv6 addresses of domain name \"$domainName\" from getIpv6AddrsOfDomainCustom:" \
"$(echo "$result" | joinLines "" "" ", " false)"
echo "$result"
return 0
fi
fi
# Option 2: Use dig
printMsgDebug "Resolving IPv6 addresses of domain name \"$domainName\" using dig"
if result="$(getIpv6AddrsOfDomainFromDig "$domainName" "${dnsServerIpv6IfSet[@]}")"; then
printMsgDebug "IPv6 addresses of domain name \"$domainName\" from dig:" \
"$(echo "$result" | joinLines "" "" ", " false)"
echo "$result"
return 0
fi
return 1
}
# getIpv6AddrsOfDomainFromDig domainName [dnsServerIpv6]
#
# Determines the given domain name's IPv6 addresses using dig
# See getIpv6AddrsOfDomain for the function contract
getIpv6AddrsOfDomainFromDig () {
local domainName="$1"; shift
local dnsServerIpv6IfSet=()
local digResponse
local -i digExitCode
if test $# -gt 0; then
dnsServerIpv6IfSet=( "@$1" ); shift
fi
digResponse="$(dig "${dnsServerIpv6IfSet[@]}" -t AAAA -q "$domainName" +noall +answer +short)" && digExitCode=$? || digExitCode=$?
# TODO More precise filtering for IPv6 addresses
# It should be okay as-is though since this is supposed to kick out domain
# names when a CNAME is found, and that case is covered as dots are not
# whitelisted
# Well, for domains that have a top level domain, anyway
echo "$digResponse" | grep -E '^[0-9abcdef:]+$'
test "$digExitCode" = 0 || return "$digExitCode"
}
# updateIpv6AddrIfRequiredDefault
#
# Retrieves the configured domain name's IPv6 addresses using the
# configured IPv6 DNS server, and determines the current local public
# IPv6 address using the configured IPv6 address detection methods
# If required, updates the domain name's IPv6 address using the
# configured update protocol and IPv6 authentication token
updateIpv6AddrIfRequiredDefault () {
updateIpv6AddrIfRequired "$updateProtocol" "$domainName" "$authTokenIpv6" "${dnsServerIpv6IfSet[@]}"
}
# updateIpv6AddrIfRequired updateProtocol domainName authTokenIpv6 [dnsServerIpv6]
#
# Retrieves the domain name's IPv6 addresses using the given IPv6 DNS
# server and determines the current local public IPv6 address using the
# configured IPv6 address detection methods
# If required, updates the domain name's IPv6 address
updateIpv6AddrIfRequired () {
local updateProtocol="$1"; shift
local domainName="$1"; shift
local authTokenIpv6="$1"; shift
local dnsServerIpv6=""
if test $# -gt 0; then
dnsServerIpv6="$1"; shift
fi
updateAddrsIfRequired "$updateProtocol" "$domainName" "" "" "$authTokenIpv6" "$dnsServerIpv6"
}
# updateIpv6AddrDefault newIpv6Addr
#
# Sets the configured domain name's IPv6 address using the configured
# update protocol and IPv6 authentication token
updateIpv6AddrDefault () {
updateIpv6Addr "$updateProtocol" "$domainName" "$authTokenIpv6" "$1"
}
# updateIpv6Addr updateProtocol domainName authTokenIpv6 newIpv6Addr
#
# Sets the domain name's IPv6 address
updateIpv6Addr () {
local updateProtocol="$1"; shift
local domainName="$1"; shift
local authTokenIpv6="$1"; shift
local newIpv6Addr="$1"; shift
updateAddrs "$updateProtocol" "$domainName" "" "" "$authTokenIpv6" "$newIpv6Addr"
}
# updateIpv6AddrWithDuckDns domainName authTokenIpv6 newIpv6Addr
#
# Sets the domain name's IPv6 address using the DuckDns protocol
# (duckdns.org)
updateIpv6AddrWithDuckDns () {
local domainName="$1"; shift
local authTokenIpv6="$1"; shift
local newIpv6Addr="$1"; shift
local authTokenIpv6Obfuscated
authTokenIpv6Obfuscated="$(getObfuscatedAuthToken "$authTokenIpv6")"
local updateUrlIpv6="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenIpv6}&ipv6=${newIpv6Addr}"
local updateUrlIpv6Obfuscated="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenIpv6Obfuscated}&ipv6=${newIpv6Addr}"
local response
local -i wgetExitCode
printMsgDebug "Calling update URL: \"$updateUrlIpv6Obfuscated\""
response="$(wget -qO - "$updateUrlIpv6" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
&& wgetExitCode="$?" || wgetExitCode="$?"
test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
printMsgDebug "Response started with \"$response\""
if ! test "$response" = "OK"; then
printMsgWarning "Unexpected response from DuckDNS dynDNS update API: \"$response\""
return 1
fi
return 0
}
# updateIpv6AddrWithFreeDns domainName authTokenIpv6 newIpv6Addr
#
# Sets the domain name's IPv6 address using either the FreeDnsV1 or
# FreeDnsV2 protocol, depending on the type of IPv6 authentication
# token given
# (freedns.afraid.org)
updateIpv6AddrWithFreeDns () {
local domainName="$1"; shift
local authTokenIpv6="$1"; shift
local newIpv6Addr="$1"; shift
if test "${#authTokenIpv6}" -eq 24; then
updateIpv6AddrWithFreeDnsV2 "$domainName" "$authTokenIpv6" "$newIpv6Addr"
else
updateIpv6AddrWithFreeDnsV1 "$domainName" "$authTokenIpv6" "$newIpv6Addr"
fi
}
# updateIpv6AddrWithFreeDnsV1 domainName authTokenIpv6 newIpv6Addr
#
# Sets the domain name's IPv6 address using the FreeDnsV1 protocol
# (freedns.afraid.org)
updateIpv6AddrWithFreeDnsV1 () {
local domainName="$1"; shift
local authTokenIpv6="$1"; shift
local newIpv6Addr="$1"; shift
local authTokenIpv6Obfuscated
authTokenIpv6Obfuscated="$(getObfuscatedAuthToken "$authTokenIpv6")"
local updateUrlIpv6="${updateUrlBaseFreeDnsV1}${authTokenIpv6}&address=${newIpv6Addr}"
local updateUrlIpv6Obfuscated="${updateUrlBaseFreeDnsV1}${authTokenIpv6Obfuscated}&address=${newIpv6Addr}"
local response
local -i wgetExitCode
printMsgDebug "Calling update URL: \"$updateUrlIpv6Obfuscated\""
response="$(wget -qO - "$updateUrlIpv6" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
&& wgetExitCode="$?" || wgetExitCode="$?"
test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
printMsgDebug "Response started with \"$response\""
if ! echo "$response" | grep -E '^Updated ' &> /dev/null \
&& ! echo "$response" | grep -E '^ERROR: Address [^ ]+ has not changed.$' &> /dev/null; then
printMsgWarning "Unexpected response from FreeDNS-v1 dynDNS update API: \"$response\""
return 1
fi
return 0
}
# updateIpv6AddrWithFreeDnsV2 domainName authTokenIpv6 newIpv6Addr
#
# Sets the domain name's IPv6 address using the FreeDnsV2 protocol
# (v6.sync.afraid.org)
updateIpv6AddrWithFreeDnsV2 () {
local domainName="$1"; shift
local authTokenIpv6="$1"; shift
local newIpv6Addr="$1"; shift
local authTokenIpv6Obfuscated
authTokenIpv6Obfuscated="$(getObfuscatedAuthToken "$authTokenIpv6")"
local updateUrlIpv6="${updateUrlBaseFreeDnsV2Ipv6}${authTokenIpv6}/?ip=${newIpv6Addr}"
local updateUrlIpv6Obfuscated="${updateUrlBaseFreeDnsV2Ipv6}${authTokenIpv6Obfuscated}/?ip=${newIpv6Addr}"
local response
local -i wgetExitCode
printMsgDebug "Calling update URL: \"$updateUrlIpv6Obfuscated\""
response="$(wget -qO - "$updateUrlIpv6" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
&& wgetExitCode="$?" || wgetExitCode="$?"
test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
printMsgDebug "Response started with \"$response\""
if ! echo "$response" | grep -E '^Updated ' &> /dev/null \
&& ! echo "$response" | grep -E '^No IP change detected for ' &> /dev/null; then
printMsgWarning "Unexpected response from FreeDNS-v2 dynDNS update API: \"$response\""
return 1
fi
return 0
}
# updateAddrsIfRequiredDefault
#
# Retrieves the configured domain name's IPv4 and IPv6 addresses and
# determines the current local public IPv4 and IPv6 addresses
# If required, updates the domain name's addresses using the configured update
# protocol and authentication tokens
updateAddrsIfRequiredDefault () {
updateAddrsIfRequired "$updateProtocol" "$domainName" "$authTokenIpv4" "$dnsServerIpv4" "$authTokenIpv6" "$dnsServerIpv6"
}
# updateAddrsIfRequired updateProtocol domainName authTokenIpv4 dnsServerIpv4 authTokenIpv6 dnsServerIpv6
#
# Retrieves the given domain name's IPv4 and IPv6 addresses and determines the
# current local public IPv4 and IPv6 addresses
# If required, updates the domain name's addresses using the given update
# protocol and authentication tokens
updateAddrsIfRequired () {
local updateProtocol="$1"; shift
local domainName="$1"; shift
local authTokenIpv4="$1"; shift
local dnsServerIpv4="$1"; shift
local dnsServerIpv4IfSet=()
if test "$dnsServerIpv4" != ""; then dnsServerIpv4IfSet+=("$dnsServerIpv4"); fi
local newIpv4Addr=""
local currentIpv4Addrs
local authTokenIpv6="$1"; shift
local dnsServerIpv6="$1"; shift
local dnsServerIpv6IfSet=()
if test "$dnsServerIpv6" != ""; then dnsServerIpv6IfSet+=("$dnsServerIpv6"); fi
local newIpv6Addr=""
local currentIpv6Addrs
printMsgDebug "Checking and if required updating the current IP addresses of domain \"${domainName}\""
# Look up the local and DNS IPv4 addresses if the IPv4 address should be maintained
if test "$authTokenIpv4" != ""; then
if ! newIpv4Addr="$(getIpv4AddrOfThisHost "${detectPublicAddrIpv4[@]}")" || test "" = "$newIpv4Addr"; then
printMsgWarning "Unable to determine this host's current IPv4 address"
fi
if ! currentIpv4Addrs="$(getIpv4AddrsOfDomain "$domainName" "${dnsServerIpv4IfSet[@]}")"; then
printMsgWarning "Unable to look up the current IPv4 addresses of \"$domainName\""
fi
printMsgDebug "The domain name \"$domainName\" has these IPv4 addresses:" \
"$(echo "$currentIpv4Addrs" | joinLines "" "" ", " false)"
fi
# Look up the local and DNS IPv6 addresses if the IPv6 address should be maintained
if test "$authTokenIpv6" != ""; then
if ! newIpv6Addr="$(getIpv6AddrOfThisHost "${detectPublicAddrIpv6[@]}")" || test "" = "$newIpv6Addr"; then
printMsgWarning "Unable to determine this host's current IPv6 address"
fi
if ! currentIpv6Addrs="$(getIpv6AddrsOfDomain "$domainName" "${dnsServerIpv6IfSet[@]}")"; then
printMsgWarning "Unable to look up the current IPv6 addresses of \"$domainName\""
fi
printMsgDebug "The domain name \"$domainName\" has these IPv6 addresses:" \
"$(echo "$currentIpv6Addrs" | joinLines "" "" ", " false)"
fi
# Determine if an update request is needed
if test "$newIpv4Addr" = "" || echo "$currentIpv4Addrs" | containsExactLine "$newIpv4Addr"; then
newIpv4Addr=""
fi
# TODO Before comparing them, normalize (expand and lower-case) the IPv6 addresses
if test "$newIpv6Addr" = "" || echo "$currentIpv6Addrs" | containsExactLine "$newIpv6Addr"; then
newIpv6Addr=""
fi
# Dispatch update request(s) if required
if test "$newIpv4Addr" != "" || test "$newIpv6Addr" != ""; then
updateAddrs "$updateProtocol" "$domainName" "$authTokenIpv4" "$newIpv4Addr" "$authTokenIpv6" "$newIpv6Addr"
else
printMsgDebug "No update required for domain name \"$domainName\""
fi
}
# updateAddrsDefault newIpv4Addr newIpv6Addr
#
# Updates the domain name's addresses to the given addresses using the
# configured update protocol and authentication tokens
updateAddrsDefault () {
updateAddrs "$updateProtocol" "$domainName" "$authTokenIpv4" "$1" "$authTokenIpv6" "$2"
}
# updateAddrs updateProtocol domainName authTokenIpv4 newIpv4Addr authTokenIpv6 newIpv6Addr
#
# Updates the domain name's addresses to the given addresses using the given
# update protocol and authentication tokens
updateAddrs () {
local updateProtocol="$1"; shift
local domainName="$1"; shift
local authTokenIpv4="$1"; shift
local newIpv4Addr="$1"; shift
local authTokenIpv6="$1"; shift
local newIpv6Addr="$1"; shift
local -i returnCode=0
if test "$newIpv4Addr" = "" && test "$newIpv6Addr" = ""; then
printMsgWarning "Neither IPv4 nor IPv6 address specified, doing nothing"
return "$returnCode"
fi
if isFunction "updateAddrsWith$updateProtocol"; then
# Protocol implementation supports updating both IPv4 and IPv6 addresses simultaneously
appendMsgInfo "Domain \"$domainName\": Updating"
test "$newIpv4Addr" != "" && appendMsgInfo "IPv4 address to \"${newIpv4Addr}\""
test "$newIpv4Addr" != "" && test "$newIpv6Addr" != "" && appendMsgInfo "and"
test "$newIpv6Addr" != "" && appendMsgInfo "IPv6 address to \"${newIpv6Addr}\""
printMsgInfo "using protocol \"$updateProtocol\""
if ! "updateAddrsWith$updateProtocol" "$domainName" "$authTokenIpv4" "$newIpv4Addr" "$authTokenIpv6" "$newIpv6Addr"; then
printMsgWarning "An error occurred while updating the IP addresses of \"$domainName\" using protocol \"$updateProtocol\""
if test "$newIpv4Addr" != ""; then returnCode=$(( returnCode | 2 )); fi
if test "$newIpv6Addr" != ""; then returnCode=$(( returnCode | 4 )); fi
fi
return "$returnCode"
else
# Protocol implementation has separate functions for updating IPv4 and IPv6 addresses
# If provided, update the IPv4 address
if test "$newIpv4Addr" != ""; then
if isFunction "updateIpv4AddrWith$updateProtocol"; then
printMsgInfo "Domain \"$domainName\": Updating IPv4 address to \"${newIpv4Addr}\" using protocol \"$updateProtocol\""
if ! "updateIpv4AddrWith$updateProtocol" "$domainName" "$authTokenIpv4" "$newIpv4Addr"; then
printMsgWarning "An error occurred while updating the IPv4 address of \"$domainName\" using protocol \"$updateProtocol\""
returnCode=$(( returnCode | 2 ))
fi
else
printMsgError "Update function not implemented: \"updateIpv4AddrWith$updateProtocol\""
returnCode=$(( returnCode | 2 ))
fi
fi
# If provided, update the IPv6 address
if test "$newIpv6Addr" != ""; then
if isFunction "updateIpv6AddrWith$updateProtocol"; then
printMsgInfo "Domain \"$domainName\": Updating IPv6 address to \"${newIpv6Addr}\" using protocol \"$updateProtocol\""
if ! "updateIpv6AddrWith$updateProtocol" "$domainName" "$authTokenIpv6" "$newIpv6Addr"; then
printMsgWarning "An error occurred while updating the IPv6 address of \"$domainName\" using protocol \"$updateProtocol\""
returnCode=$(( returnCode | 4 ))
fi
else
printMsgError "Update function not implemented: \"updateIpv6AddrWith$updateProtocol\""
returnCode=$(( returnCode | 4 ))
fi
fi
return $returnCode
fi
}
# updateAddrsWithDeSec domainName authTokenIpv4 newIpv4Addr authTokenIpv6 newIpv6Addr
#
# Sets the domain name's IPv4 and IPv6 address using the deSEC protocol
# (update.dedyn.io)
updateAddrsWithDeSec () {
local domainName="$1"; shift
local authTokenIpv4="$1"; shift
local newIpv4Addr="$1"; shift
local authTokenIpv6="$1"; shift
local newIpv6Addr="$1"; shift
local authToken
local authTokenObfuscated
local updateUrl
local httpHeader
local httpHeaderObfuscated
local response
local -i wgetExitCode
# Determine the single API authentication token
if test "$authTokenIpv4" != "" && test "$authTokenIpv6" != ""; then
if test "$authTokenIpv4" != "$authTokenIpv6"; then
printMsgWarning "Authentication tokens for deSEC API differ, using the one for IPv4"
fi
authToken="$authTokenIpv4"
elif test "$authTokenIpv4" != ""; then
authToken="$authTokenIpv4"
elif test "$authTokenIpv6" != ""; then
authToken="$authTokenIpv6"
else
printMsgError "No authentication tokens specified"
return 1
fi
authTokenObfuscated="$(getObfuscatedAuthToken "$authToken")"
# Make sure we have some IP addresses that should be sent to the API
if test "$newIpv4Addr" = "" && test "$newIpv6Addr" = ""; then
printMsgWarning "Neither an IPv4 nor an IPv6 address was supplied, doing nothing"
return 0
fi
# Do not unset IP addresses that have not been specified
test "$newIpv4Addr" = "" && newIpv4Addr="preserve"
test "$newIpv6Addr" = "" && newIpv6Addr="preserve"
# Build the update URL and the HTTP header entry
updateUrl="${updateUrlBaseDeSec}hostname=${domainName}&ip=${newIpv4Addr}&ipv6=${newIpv6Addr}"
httpHeader="Authorization: Token $authToken"
httpHeaderObfuscated="Authorization: Token $authTokenObfuscated"
# Dispatch the update request
printMsgDebug "Calling update URL \"$updateUrl\" with extra HTTP header line \"${httpHeaderObfuscated}\""
response="$(wget -qO - --header="$httpHeader" "$updateUrl" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
&& wgetExitCode="$?" || wgetExitCode="$?"
test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
printMsgDebug "Response started with \"$response\""
if test "$response" != "good"; then
printMsgWarning "Unexpected response from deSEC dynDNS update API: \"$response\""
return 1
fi
return 0
}
# getTxtRecordsOfDomainDefault
#
# Determines the configured domain name's TXT records using the
# configured TXT record DNS server
getTxtRecordsOfDomainDefault () {
getTxtRecordsOfDomain "$domainName" "${dnsServerTxtIfSet[@]}"
}
# getTxtRecordsOfDomain domainName [dnsServerTxt]
#
# Determines the given domain name's TXT records
# The records are returned in a newline-separated list
# The list is empty if the domain name does not have any TXT records
# Returns with a non-zero code if an error occurred while retrieving the
# domain name's TXT records
getTxtRecordsOfDomain () {
local domainName="$1"; shift
local dnsServerTxtIfSet=()
local commandType
local result
if test $# -gt 0; then
dnsServerTxtIfSet=( "$1" ); shift
fi
# Option 1: Ask the custom implementation if available
if isFunction "getTxtRecordsOfDomainCustom"; then
printMsgDebug "Retrieving TXT records of domain name \"$domainName\" using getTxtRecordsOfDomainCustom"
if result="$(getTxtRecordsOfDomainCustom "$domainName" "${dnsServerTxtIfSet[@]}")"; then
printMsgDebug "TXT records of domain name \"$domainName\" from getTxtRecordsOfDomainCustom:" \
"$(echo "$result" | joinLines "\"" "\"" "\", \"" false)"
echo "$result"
return 0
fi
fi
# Option 2: Use dig
printMsgDebug "Retrieving TXT records of domain name \"$domainName\" using dig"
if result="$(getTxtRecordsOfDomainFromDig "$domainName" "${dnsServerTxtIfSet[@]}")"; then
printMsgDebug "TXT records of domain name \"$domainName\" from dig:" \
"$(echo "$result" | joinLines "\"" "\"" "\", \"" false)"
echo "$result"
return 0
fi
return 1
}
# getTxtRecordsOfDomainFromDig domainName [dnsServerTxt]
#
# Determines the given domain name's TXT records using dig
# See getTxtRecordsOfDomain for the function contract
getTxtRecordsOfDomainFromDig () {
local domainName="$1"; shift
local dnsServerTxtIfSet=()
if test $# -gt 0; then
dnsServerTxtIfSet=( "@$1" ); shift
fi
# TODO Fix: For a domain name that has 0 TXT records this returns a
# single empty line ("<LF>"), but should instead return nothing ("")
dig "${dnsServerTxtIfSet[@]}" -t TXT -q "$domainName" +noall +answer +short \
| sed -re 's/^"([^"]*)"$/\1/'
}
# addTxtRecordDefault text
#
# Adds the given TXT record to the configured domain name using the
# configured update protocol and TXT authentication token
addTxtRecordDefault () {
addTxtRecord "$updateProtocol" "$domainName" "$authTokenTxt" "$1"
}
# addTxtRecord updateProtocol domainName authTokenTxt text
#
# Adds the given TXT record to the domain name
addTxtRecord () {
local updateProtocol="$1"; shift
local domainName="$1"; shift
local authTokenTxt="$1"; shift
local text="$1"; shift
local addFunction="addTxtRecordWith$updateProtocol"
if ! isFunction "$addFunction"; then
printMsgError "Function not implemented: \"${addFunction}\""
return 1
fi
"$addFunction" "$domainName" "$authTokenTxt" "$text"
}
# addTxtRecordWithDuckDns domainName authTokenTxt text
#
# Adds the given TXT record to the domain name using the DuckDns
# protocol (duckdns.org)
addTxtRecordWithDuckDns () {
local domainName="$1"; shift
local authTokenTxt="$1"; shift
local text="$1"; shift
local authTokenTxtObfuscated
authTokenTxtObfuscated="$(getObfuscatedAuthToken "$authTokenTxt")"
local updateUrlTxt="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenTxt}&txt=${text}"
local updateUrlTxtObfuscated="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenTxtObfuscated}&txt=${text}"
local response
local -i wgetExitCode
printMsgDebug "Calling update URL: \"$updateUrlTxtObfuscated\""
response="$(wget -qO - "$updateUrlTxt" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
&& wgetExitCode="$?" || wgetExitCode="$?"
test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
printMsgDebug "Response started with \"$response\""
if ! test "$response" = "OK"; then
printMsgWarning "Unexpected response from DuckDNS DNS TXT record update API: \"$response\""
return 1
fi
return 0
}
# removeTxtRecordDefault text
#
# Removes the given TXT record from the configured domain name using the
# configured update protocol and TXT authentication token
removeTxtRecordDefault () {
removeTxtRecord "$updateProtocol" "$domainName" "$authTokenTxt" "$1"
}
# removeTxtRecord updateProtocol domainName authTokenTxt text
#
# Removes the given TXT record from the domain name
removeTxtRecord () {
local updateProtocol="$1"; shift
local domainName="$1"; shift
local authTokenTxt="$1"; shift
local text="$1"; shift
local removeFunction="removeTxtRecordWith$updateProtocol"
if ! isFunction "$removeFunction"; then
printMsgError "Function not implemented: \"${removeFunction}\""
return 1
fi
"$removeFunction" "$domainName" "$authTokenTxt" "$text"
}
# removeTxtRecordWithDuckDns domainName authTokenTxt text
#
# Removes the given TXT record from the domain name using the DuckDns
# protocol (duckdns.org)
removeTxtRecordWithDuckDns () {
local domainName="$1"; shift
local authTokenTxt="$1"; shift
local text="$1"; shift
local authTokenTxtObfuscated
authTokenTxtObfuscated="$(getObfuscatedAuthToken "$authTokenTxt")"
local updateUrlTxt="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenTxt}&txt=${text}&clear=true"
local updateUrlTxtObfuscated="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenTxtObfuscated}&txt=${text}&clear=true"
local response
local -i wgetExitCode
printMsgDebug "Calling update URL: \"$updateUrlTxtObfuscated\""
response="$(wget -qO - "$updateUrlTxt" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
&& wgetExitCode="$?" || wgetExitCode="$?"
test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
printMsgDebug "Response started with \"$response\""
if ! test "$response" = "OK"; then
printMsgWarning "Unexpected response from DuckDNS DNS TXT record update API: \"$response\""
return 1
fi
return 0
}
# Declare some global statics
declare updateUrlBaseFreeDnsV1="https://freedns.afraid.org/dynamic/update.php?"
declare updateUrlBaseFreeDnsV2Ipv4="https://sync.afraid.org/u/"
declare updateUrlBaseFreeDnsV2Ipv6="https://v6.sync.afraid.org/u/"
declare updateUrlBaseDuckDns="https://www.duckdns.org/update?"
declare updateUrlBaseDeSec="https://update.dedyn.io?"
declare sleepTimeDefault="2m"
# Handle missing arguments
if test $# -eq 0; then
printHelpMessage
exit 1
fi
# Handle --help as single argument
if test $# -eq 1 && test "$1" = "--help"; then
printHelpMessage
exit 0
fi
# Catch --version as single argument
if test $# -eq 1 && test "$1" = "--version"; then
getVersion
exit 0
fi
# Make sure the absolutely required programs are available
allRequiredProgramsPresent=true
type find &> /dev/null || { printMsgError "Required program \"find\" is not available"; allRequiredProgramsPresent=false; }
type grep &> /dev/null || { printMsgError "Required program \"grep\" is not available"; allRequiredProgramsPresent=false; }
type sed &> /dev/null || { printMsgError "Required program \"sed\" is not available"; allRequiredProgramsPresent=false; }
$allRequiredProgramsPresent || exit 1
unset allRequiredProgramsPresent
# Set default values for certain options
authTokenIpv4=""
authTokenIpv6=""
dnsServer=""
dnsServerIpv4=""
dnsServerIpv6=""
dnsServerTxt=""
oneShot=false
sleepTime="$sleepTimeDefault"
verbose=false
listFunctions=false
if $upnpAvailable; then
detectPublicAddrIpv4=( "NetDev" "Upnp" )
else
detectPublicAddrIpv4=( "NetDev" )
fi
detectPublicAddrIpv6=( "NetDev" )
# Scan the arguments for --verbose up front and set $verbose accordingly
for argument in "$@"; do
if test "$argument" = "--verbose"; then
verbose=true
break
fi
done
unset argument
# Source the configuration files
while read -rs configurationFile; do
printMsgInfo "Sourcing configuration file \"$configurationFile\""
# shellcheck source=/dev/null
if ! source -- "$configurationFile"; then
printMsgError "Failed to source configuration file \"${configurationFile}\""
exit 1
fi
done < <(getConfigurationFilesSystem; getConfigurationFilesUser)
unset configurationFile
# Read and parse the arguments
# First of all, see if one or more configuration files are specified,
# and if so, source them all in the order in which they are given
nextArgIsValueForConfigFile=false
for argument in "$@"; do
if $nextArgIsValueForConfigFile; then
printMsgInfo "Sourcing argument-supplied configuration file \"$argument\""
# shellcheck source=/dev/null
if ! source -- "$argument"; then
printMsgError "Failed to source configuration file \"${argument}\""
exit 1
fi
elif test "$argument" = "--config-file"; then
nextArgIsValueForConfigFile=true
continue
fi
nextArgIsValueForConfigFile=false
done
if $nextArgIsValueForConfigFile; then
printMsgError "Trailing argument has no value: \"$argument\""
exit 1
fi
unset nextArgIsValueForConfigFile
unset argument
# Parse the other arguments, so that they overwrite whatever was
# set in the sourced configuration file(s)
while test $# -gt 0; do
# Arguments without values (boolean options)
if test "--one-shot" = "$1"; then
oneShot=true; shift
elif test "--verbose" = "$1"; then
verbose=true; shift
elif test "--list-functions" = "$1"; then
listFunctions=true; shift
# Special arguments that are handled elsewhere
elif test "--help" = "$1"; then
printMsgError "Special argument \"--help\" must be given as single argument"
exit 1
elif test "--version" = "$1"; then
printMsgError "Special argument \"--version\" must be given as single argument"
exit 1
# Arguments with a single value (here, this also means arguments
# that take a value composed of comma-separated subvalues, which
# are parsed into a bash array)
elif test $# -lt 2; then
printMsgError "Trailing argument has no value: \"$1\""
exit 1
elif test "--auth-token-ipv4" = "$1"; then
shift
authTokenIpv4="$1"; shift
elif test "--auth-token-ipv6" = "$1"; then
shift
authTokenIpv6="$1"; shift
elif test "--call-function" = "$1"; then
shift
callFunction="$1"; shift
# From here on it is any number of trailing function arguments,
# therefore cease argument parsing
break
elif test "--config-file" = "$1"; then
# Skip --config-file and its value because all config files have
# already been sourced
shift 2
elif test "--detect-public-addr-ipv4" = "$1"; then
shift
detectPublicAddrIpv4=()
while read -rsd ',' currentWord || test "$currentWord" != ""; do
detectPublicAddrIpv4+=( "$currentWord" )
done < <(echo -n "$1")
shift
elif test "--detect-public-addr-ipv6" = "$1"; then
shift
detectPublicAddrIpv6=()
while read -rsd ',' currentWord || test "$currentWord" != ""; do
detectPublicAddrIpv6+=( "$currentWord" )
done < <(echo -n "$1")
shift
elif test "--dns-server" = "$1"; then
shift
dnsServer="$1"; shift
elif test "--dns-server-ipv4" = "$1"; then
shift
dnsServerIpv4="$1"; shift
elif test "--dns-server-ipv6" = "$1"; then
shift
dnsServerIpv6="$1"; shift
elif test "--domain-name" = "$1"; then
shift
domainName="$1"; shift
elif test "--update-protocol" = "$1"; then
shift
updateProtocol="$1"; shift
elif test "--sleep-time" = "$1"; then
shift
sleepTime="$1"; shift
else
printMsgError "Unknown argument \"$1\""
exit 1
fi
done
# Set some derived variables
if test "$dnsServer" != ""; then
test "$dnsServerIpv4" = "" && dnsServerIpv4="$dnsServer"
test "$dnsServerIpv6" = "" && dnsServerIpv6="$dnsServer"
test "$dnsServerTxt" = "" && dnsServerTxt="$dnsServer"
fi
# Some optional options are also available as arrays so that they can
# be used for function calls without if-else constructs
test "$dnsServerIpv4" != "" && dnsServerIpv4IfSet=("$dnsServerIpv4") || dnsServerIpv4IfSet=()
test "$dnsServerIpv6" != "" && dnsServerIpv6IfSet=("$dnsServerIpv6") || dnsServerIpv6IfSet=()
test "$dnsServerTxt" != "" && dnsServerTxtIfSet=("$dnsServerTxt") || dnsServerTxtIfSet=()
# Validate some very basic easy-to-validate options
if ! test "$verbose" = true && ! test "$verbose" = false; then
printMsgError "--verbose, if set in a configuration file, must be either \"true\" or \"false\", was \"$verbose\""
exit 1
fi
# If verbose mode is enabled, report various programs that the
# application can run without, albeit with reduced functionality
if $verbose; then
$upnpAvailable || printMsgDebug "Public IPv4 address detection method \"Upnp\" cannot" \
"be used because the program \"upnpc\" is not available"
type bc &> /dev/null || { printMsgDebug "Program \"bc\" not available, some functionality may be compromised"; }
type dig &> /dev/null || { printMsgDebug "Program \"dig\" not available, some functionality may be compromised"; }
type head &> /dev/null || { printMsgDebug "Program \"head\" not available, some functionality may be compromised"; }
type ip &> /dev/null || { printMsgDebug "Program \"ip\" not available, some functionality may be compromised"; }
type sleep &> /dev/null || { printMsgDebug "Program \"sleep\" not available, some functionality may be compromised"; }
type tail &> /dev/null || { printMsgDebug "Program \"tail\" not available, some functionality may be compromised"; }
type wget &> /dev/null || { printMsgDebug "Program \"wget\" not available, some functionality may be compromised"; }
fi
# Special functionality --list-functions
if ! test "$listFunctions" = true && ! test "$listFunctions" = false; then
printMsgError "--list-functions, if set in a configuration file, must be either \"true\" or \"false\", was \"$listFunctions\""
exit 1
fi
if $listFunctions; then
echo -n "Functions that will be used if they are declared:
getIpv4AddrsOfDomainCustom domainName [dnsServerIpv4]
getIpv6AddrsOfDomainCustom domainName [dnsServerIpv6]
getTxtRecordsOfDomainCustom domainName [dnsServerTxt]
"
echo "Functions declared in this script or in one of its configuration files:"
# Print the functions, and also append information about the arguments they
# are called with if known
declare -F | sed -re 's/^declare -f //' | sort | sed -re \
's/^(addTxtRecord)$/\1 updateProtocol domainName authTokenTxt text/;'\
's/^(addTxtRecordDefault)$/\1 text/;'\
's/^(addTxtRecordWith[^ ]+)$/\1 domainName authTokenTxt text/;'\
's/^(appendMsg[^ ]+)$/\1 [text].../;'\
's/^(containsExactLine)$/\1 lineToFind (reads STDIN)/;'\
's/^(getIpv4AddrAsHexString)$/\1 ipv4Addr/;'\
's/^(getIpv4AddrOfThisHost)$/\1 [method[@@argument]...]/;'\
's/^(getIpv4AddrOfThisHostFromFile)$/\1 path/;'\
's/^(getIpv4AddrOfThisHostFromNetDev)$/\1 [networkDevice]/;'\
's/^(getIpv4AddrOfThisHostFromUrl)$/\1 url/;'\
's/^(getIpv4AddrsOfDomain)$/\1 domainName [dnsServerIpv4]/;'\
's/^(getIpv4AddrsOfDomainCustom)$/\1 domainName [dnsServerIpv4]/;'\
's/^(getIpv4AddrsOfDomainFrom[^ ]*)$/\1 domainName [dnsServerIpv4]/;'\
's/^(getIpv6AddrOfThisHost)$/\1 [method[@@argument]...]/;'\
's/^(getIpv6AddrOfThisHostFromFile)$/\1 path/;'\
's/^(getIpv6AddrOfThisHostFromNetDev)$/\1 [networkDevice]/;'\
's/^(getIpv6AddrOfThisHostFromUrl)$/\1 url/;'\
's/^(getIpv6AddrsOfDomain)$/\1 domainName [dnsServerIpv6]/;'\
's/^(getIpv6AddrsOfDomainCustom)$/\1 domainName [dnsServerIpv6]/;'\
's/^(getIpv6AddrsOfDomainFrom[^ ]*)$/\1 domainName [dnsServerIpv6]/;'\
's/^(getIpv6AddrAsHexString)$/\1 ipv6Addr/;'\
's/^(getIpv6AddrExpanded)$/\1 ipv6Addr/;'\
's/^(getMethodArgument)$/\1 method@@argument/;'\
's/^(getMethodName)$/\1 method[@@argument]/;'\
's/^(getObfuscatedAuthToken)$/\1 authToken/;'\
's/^(getPublicIpv.AddrFromStream)$/\1 (reads STDIN)/;'\
's/^(getTxtRecordsOfDomain)$/\1 domainName [dnsServerTxt]/;'\
's/^(getTxtRecordsOfDomainCustom)$/\1 domainName [dnsServerTxt]/;'\
's/^(getTxtRecordsOfDomainFrom[^ ]*)$/\1 domainName [dnsServerTxt]/;'\
's/^(isBetterLifetime)$/\1 lifetimeToTest lifetimeReference/;'\
's/^(isFunction)$/\1 command/;'\
's/^(isMethodWithArgument)$/\1 method[@@argument]/;'\
's/^(isPublicIpv4Addr)$/\1 ipv4Addr/;'\
's/^(isPublicIpv6Addr)$/\1 ipv6Addr/;'\
's/^(isStringInRange)$/\1 lowerBorderIncluding upperBorderExcluding string/;'\
's/^(isWhitelistedExitCode)$/\1 exitCode [whitelistedExitCode...]/;'\
's/^(joinLines)$/\1 prefix suffix separator [appendFinalNewline] (reads STDIN)/;'\
's/^(printMsg)$/\1 levelPrefix [text].../;'\
's/^(printMsg[^ ]+)$/\1 [text].../;'\
's/^(removeTxtRecord)$/\1 updateProtocol domainName authTokenTxt text/;'\
's/^(removeTxtRecordDefault)$/\1 text/;'\
's/^(removeTxtRecordWith[^ ]+)$/\1 domainName authTokenTxt text/;'\
's/^(splitBySubstring)$/\1 string substring/;'\
's/^(updateAddrs)$/\1 updateProtocol domainName authTokenIpv4 newIpv4Addr authTokenIpv6 newIpv6Addr/;'\
's/^(updateAddrsDefault)$/\1 newIpv4Addr newIpv6Addr/;'\
's/^(updateAddrsIfRequired)$/\1 updateProtocol domainName authTokenIpv4 dnsServerIpv4 authTokenIpv6 dnsServerIpv6/;'\
's/^(updateAddrsWith[^ ]+)$/\1 domainName authTokenIpv4 newIpv4Addr authTokenIpv6 newIpv6Addr/;'\
's/^(updateIpv4Addr)$/\1 updateProtocol domainName authTokenIpv4 newIpv4Addr/;'\
's/^(updateIpv4AddrDefault)$/\1 newIpv4Addr/;'\
's/^(updateIpv4AddrIfRequired)$/\1 updateProtocol domainName authTokenIpv4 [dnsServerIpv4]/;'\
's/^(updateIpv4AddrWith[^ ]+)$/\1 domainName authTokenIpv4 newIpv4Addr/;'\
's/^(updateIpv6Addr)$/\1 updateProtocol domainName authTokenIpv6 newIpv6Addr/;'\
's/^(updateIpv6AddrDefault)$/\1 newIpv6Addr/;'\
's/^(updateIpv6AddrIfRequired)$/\1 updateProtocol domainName authTokenIpv6 [dnsServerIpv6]/;'\
's/^(updateIpv6AddrWith[^ ]+)$/\1 domainName authTokenIpv6 newIpv6Addr/;'\
's/^(.*)$/ \1/'
exit 0
fi
# Validate options that are required for, or typically relevant for
# --call-function
declare methodString
declare methodName
declare functionName
declare validMethod
for methodString in "${detectPublicAddrIpv4[@]}"; do
if isMethodWithArgument "$methodString"; then
methodName="$(getMethodName "$methodString")"
else
methodName="$methodString"
fi
functionName="getIpv4AddrOfThisHostFrom$methodName"
isFunction "$functionName" && continue
appendMsgError "--detect-public-addr-ipv4: Invalid public IPv4 address detection method \"$methodName\" specified. Available method(s):"
while read -rs validMethod; do
appendMsgError "\"$validMethod\""
done < <(declare -F | sed -re 's/^declare -f //' \
| { grep '^getIpv4AddrOfThisHostFrom' || true; } \
| sed -re 's/^getIpv.AddrOfThisHostFrom//')
printMsgError
exit 1
done
for methodString in "${detectPublicAddrIpv6[@]}"; do
if isMethodWithArgument "$methodString"; then
methodName="$(getMethodName "$methodString")"
else
methodName="$methodString"
fi
functionName="getIpv6AddrOfThisHostFrom$methodName"
isFunction "$functionName" && continue
appendMsgError "--detect-public-addr-ipv6: Invalid public IPv6 address detection method \"$methodName\" specified. Available method(s):"
while read -rs validMethod; do
appendMsgError "\"$validMethod\""
done < <(declare -F | sed -re 's/^declare -f //' \
| { grep '^getIpv6AddrOfThisHostFrom' || true; } \
| sed -re 's/^getIpv.AddrOfThisHostFrom//')
printMsgError
exit 1
done
unset methodString
unset methodName
unset functionName
unset validMethod
# Special functionality --call-function
if ! test -z ${callFunction+x}; then
printMsgDebug "Calling function \"$callFunction\" with $# argument(s)"
# Find out if this is a function that is supposed to return a
# boolean result, i.e. whose return code is its only result
booleanResultFunction=false
if echo "$callFunction" | grep -E '^is|^contains' &> /dev/null; then
booleanResultFunction=true
fi
# Call the function and capture its return code
declare -i returnCode
"$callFunction" "$@" && returnCode="$?" || returnCode="$?"
# Provide some extra output regarding the function's return code
if $booleanResultFunction; then
if test "$returnCode" -eq 0; then
# Regular true return
printMsgDebug "Boolean function returned true (code ${returnCode})"
elif test "$returnCode" -eq 1; then
# Regular false return
printMsgDebug "Boolean function returned false (code ${returnCode})"
else
# Uh-oh, possibly some unhandled error
printMsgWarning "Boolean function \"$callFunction\" returned false with unusual code ${returnCode}"
fi
else
if test "$returnCode" -eq 0; then
printMsgDebug "Function returned with code $returnCode"
else
# Be a bit more panicky if a non-boolean function returned
# with a bad code
printMsgWarning "Function \"$callFunction\" returned with code $returnCode"
fi
fi
exit "$returnCode"
fi
# Validate arguments required for main functionality
if test -z ${domainName+x}; then
printMsgError "No domain name specified (--domain-name)"
exit 1
fi
if test "$authTokenIpv4" = "" && test "$authTokenIpv6" = ""; then
printMsgError "No authentication token specified (--auth-token-ipv4 or --auth-token-ipv6)"
exit 1
fi
if test -z ${updateProtocol+x}; then
printMsgError "No update protocol specified (--update-protocol)"
exit 1
fi
if test "$authTokenIpv4" != "" && test ${#detectPublicAddrIpv4[@]} -eq 0; then
printMsgError "IPv4 authentication token configured, but no method(s) for IPv4 address detection of this host specified (--detect-public-addr-ipv4)"
exit 1
fi
if test "$authTokenIpv4" != "" && ! isFunction "updateIpv4AddrWith$updateProtocol" && ! isFunction "updateAddrsWith$updateProtocol"; then
printMsgError "IPv4 authentication token configured, but no update function for IPv4 and protocol \"$updateProtocol\" implemented:" \
"\"updateIpv4AddrWith$updateProtocol\", \"updateAddrsWith$updateProtocol\""
exit 1
fi
if test "$authTokenIpv6" != "" && test ${#detectPublicAddrIpv6[@]} -eq 0; then
printMsgError "IPv6 authentication token configured, but no method(s) for IPv6 address detection of this host specified (--detect-public-addr-ipv6)"
exit 1
fi
if test "$authTokenIpv6" != "" && ! isFunction "updateIpv6AddrWith$updateProtocol" && ! isFunction "updateAddrsWith$updateProtocol"; then
printMsgError "IPv6 authentication token configured, but no update function for IPv6 and protocol \"$updateProtocol\" implemented:" \
"\"updateIpv6AddrWith$updateProtocol\", \"updateAddrsWith$updateProtocol\""
exit 1
fi
if ! test "$oneShot" = true && ! test "$oneShot" = false; then
printMsgError "--one-shot, if set in a configuration file, must be either \"true\" or \"false\", was \"$oneShot\""
exit 1
fi
# All arguments validated and ready to go; print a summary
printMsgInfo "This is yabddnsd $(getVersion)"
printMsgInfo "Domain name: \"${domainName}\""
printMsgInfo "Update protocol: \"$updateProtocol\""
test "$authTokenIpv4" != "" && printMsgInfo "IPv4 authentication token: \"$(getObfuscatedAuthToken "$authTokenIpv4")\""
test "$authTokenIpv6" != "" && printMsgInfo "IPv6 authentication token: \"$(getObfuscatedAuthToken "$authTokenIpv6")\""
declare method
if test "$authTokenIpv4" != "" && test ${#detectPublicAddrIpv4[@]} -gt 0; then
appendMsgInfo "Public IPv4 address detection method(s):"
for method in "${detectPublicAddrIpv4[@]}"; do
appendMsgInfo "\"$method\""
done
printMsgInfo
fi
if test "$authTokenIpv6" != "" && test ${#detectPublicAddrIpv6[@]} -gt 0; then
appendMsgInfo "Public IPv6 address detection method(s):"
for method in "${detectPublicAddrIpv6[@]}"; do
appendMsgInfo "\"$method\""
done
printMsgInfo
fi
unset method
test "$dnsServerIpv4" != "" && printMsgInfo "IPv4 DNS server: \"${dnsServerIpv4}\""
test "$dnsServerIpv6" != "" && printMsgInfo "IPv6 DNS server: \"${dnsServerIpv6}\""
test "$dnsServerTxt" != "" && printMsgInfo "TXT record DNS server: \"${dnsServerIpv6}\""
if $oneShot; then
printMsgInfo "One-shot operation (--one-shot) is active: Dispatching at most a single update"
else
printMsgInfo "Sleeping time between iterations: \"${sleepTime}\""
fi
printMsgDebug "Verbose output is enabled"
# Handle one-shot operation
if $oneShot; then
updateAddrsIfRequiredDefault
exit $?
fi
# Get going
printMsgInfo "Entering main loop"
while true ; do
updateAddrsIfRequiredDefault || true
printMsgDebug "Sleeping for \"$sleepTime\""
if ! sleep "$sleepTime"; then
printMsgWarning "Calling sleep with argument \"${sleepTime}\" returned with a non-zero code, calling it with \"${sleepTimeDefault}\" instead"
sleep "$sleepTimeDefault"
fi
done