#!/bin/bash # SPDX-License-Identifier: AGPL-3.0-or-later ############################################################################ # # # This script is a wrapper around ez-ipupdate and/or wget # # to update a Dynamic DNS account. The script is used as an # # interface between plinth and ez-ipupdate # # the script will store configuration, return configuration # # to plinth UI and do a dynamic DNS update. The script will # # also determe if we are behind a NAT device, if we can use # # ez-ipupdate tool or if we need to do some wget magic # # # # Todo: IPv6 # # Todo: GET WAN IP from Router via UPnP if supported # # Todo: licence string? # # author: Daniel Steglich # # # ############################################################################ PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin # static values WGET=$(which wget) WGETOPTIONS="-o /dev/null -t 3 -T 3" EMPTYSTRING="none" NOIP="0.0.0.0" # how often do we poll for IP changes if we are behind a NAT? UPDATEMINUTES=5 # if we do not have a URL to look up public IP, how often should we do # a "blind" update UPDATEMINUTESUNKNOWN=3600 TOOLNAME=ez-ipupdate UPDATE_TOOL=$(which ${TOOLNAME}) DISABLED_STRING='disabled' ENABLED_STRING='enabled' # Dirs and filenames CFGDIR="/etc/${TOOLNAME}/" CFG="${CFGDIR}${TOOLNAME}.conf" CFG_disabled="${CFGDIR}${TOOLNAME}.inactive" IPFILE="${CFGDIR}${TOOLNAME}.currentIP" STATUSFILE="${CFGDIR}${TOOLNAME}.status" LASTUPDATE="${CFGDIR}/last-update" HELPERCFG="${CFGDIR}${TOOLNAME}-plinth.cfg" CRONJOB="/etc/cron.d/${TOOLNAME}" PIDFILE="/var/run/ez-ipupdate.pid" # this function will parse commandline options doGetOpt() { basicauth=0 ignoreCertError=0 useIPv6=0 while getopts ":s:d:u:P:I:U:c:b:6:p" opt; do case ${opt} in s) if [ "${OPTARG}" != "${EMPTYSTRING}" ];then server=${OPTARG} else server="" fi ;; d) host=${OPTARG} ;; u) user=${OPTARG} ;; P) pass=${OPTARG} ;; p) if read -t 0; then IFS= read -r pass fi ;; I) if [ "${OPTARG}" != "${EMPTYSTRING}" ];then ipurl=${OPTARG} else ipurl="" fi ;; U) if [ "${OPTARG}" != "${EMPTYSTRING}" ];then updateurl=${OPTARG} else updateurl="" fi ;; b) basicauth=${OPTARG} ;; c) ignoreCertError=${OPTARG} ;; 6) useIPv6=${OPTARG} ;; \?) echo "Invalid option: -${OPTARG}" >&2 exit 1 ;; esac done } # this function will write a persistent config file to disk doWriteCFG() { mkdir ${CFGDIR} 2> /dev/null # always write to the inactive config - needs to be enabled via "start" command later local out_file=${CFG_disabled} # reset the last update time echo 0 > ${LASTUPDATE} # reset the last updated IP echo "0.0.0.0" > ${IPFILE} # reset last update (if there is one) rm ${STATUSFILE} 2> /dev/null # find the interface (always the default gateway interface) default_interface=$(ip route |grep default |awk '{print $5}') # store the given options in ez-ipupdate compatible config file { echo "host=${host}" echo "server=${server}" echo "user=${user}:${pass}" echo "service-type=gnudip" echo "retrys=3" echo "wildcard" } > ${out_file} # store UPDATE URL params { echo "POSTURL ${updateurl}" echo "POSTAUTH ${basicauth}" echo "POSTSSLIGNORE ${ignoreCertError}" echo "POSTUSEIPV6 ${useIPv6}" } > ${HELPERCFG} # check if we are behind a NAT Router echo "IPURL ${ipurl}" >> ${HELPERCFG} if [ -z ${ipurl} ];then echo "NAT unknown" >> ${HELPERCFG} else doGetWANIP ISGLOBAL=$(ip addr ls "${default_interface}" | grep "${wanip}") if [ -z "${ISGLOBAL}" ];then # we are behind NAT echo "NAT yes" >> ${HELPERCFG} else # we are directly connected echo "NAT no" >> ${HELPERCFG} # if this file is added ez-ipupdate will take ip form this interface { echo "interface=${default_interface}" # if this line is added to config file, ez-ipupdate will be launched on startup via init.d echo "daemon" echo "execute=${0} success" } >> ${out_file} fi fi } # this function will read the config file from disk # special treatment for empty strings is done here: # plinth will give empty strings like: '' # but we don't want this single quotes to be used doReadCFG() { host="" server="" user="" pass="" ipurl="" if [ ! -z "${cfgfile}" ];then host=$(grep ^host= "${cfgfile}" 2> /dev/null | cut -d = -f 2-) server=$(grep ^server= "${cfgfile}" 2> /dev/null | cut -d = -f 2- | grep -v ^\'\') user=$(grep ^user= "${cfgfile}" 2> /dev/null | cut -d = -f 2- | cut -d : -f 1 ) pass=$(grep ^user= "${cfgfile}" 2> /dev/null | cut -d = -f 2- | cut -d : -f 2-) fi if [ ! -z ${HELPERCFG} ];then ipurl=$(grep ^IPURL "${HELPERCFG}" 2> /dev/null |awk '{print $2}' |grep -v ^\'\') updateurl=$(grep POSTURL "${HELPERCFG}" 2> /dev/null |awk '{print $2}' |grep -v ^\'\') basicauth=$(grep POSTAUTH "${HELPERCFG}" 2> /dev/null |awk '{print $2}' |grep -v ^\'\') ignoreCertError=$(grep POSTSSLIGNORE "${HELPERCFG}" 2> /dev/null |awk '{print $2}' |grep -v ^\'\') useIPv6=$(grep POSTUSEIPV6 "${HELPERCFG}" 2> /dev/null |awk '{print $2}' |grep -v ^\'\') fi } # replace vars from url: i.e.: # https://example.com/update.php?domain=&User=&Pass= # also this function will remove the surounding single quotes from the URL string # as plinth will add them doReplaceVars() { local url=$(echo "${updateurl}" | sed "s//${wanip}/g") url=$(echo "${url}" | sed "s//${host}/g") url=$(echo "${url}" | sed "s//${user}/g") url=$(echo "${url}" | sed "s//${pass}/g") url=$(echo "${url}" | sed "s/'//g") updateurl=${url} logger "expanded update URL as ${url}" } # doReadCFG() needs to be run before this # this function will return all configured parameters in a way that # plinth will understand (plinth know the order of # parameters this function will return) doStatus() { PROC=$(pgrep ${TOOLNAME}) if [ -f "${CRONJOB}" ];then echo "${ENABLED_STRING}" elif [ ! -z "${PROC}" ];then echo "${ENABLED_STRING}" else echo "${DISABLED_STRING}" fi if [ ! -z "${server}" ];then echo "${server}" else echo "${DISABLED_STRING}" fi if [ ! -z "${host}" ];then echo "${host}" else echo "${DISABLED_STRING}" fi if [ ! -z "${user}" ];then echo "${user}" else echo "${DISABLED_STRING}" fi if [ ! -z "${pass}" ];then echo "${pass}" else echo "${DISABLED_STRING}" fi if [ ! -z "${ipurl}" ];then echo "${ipurl}" else echo "${DISABLED_STRING}" fi if [ ! -z "${updateurl}" ];then echo "${updateurl}" else echo "${DISABLED_STRING}" fi if [ ! -z "${ignoreCertError}" ];then echo "${ignoreCertError}" else echo "${DISABLED_STRING}" fi if [ ! -z "${basicauth}" ];then echo "${basicauth}" else echo "${DISABLED_STRING}" fi if [ ! -z "${useIPv6}" ];then echo "${useIPv6}" else echo "${DISABLED_STRING}" fi } # ask a public WEB Server for the WAN IP we are comming from # and store this ip within $wanip doGetWANIP() { if [ ! -z "${ipurl}" ];then local wgetoptions="${WGETOPTIONS}" if [ "${useIPv6}" = "enabled" ];then wgetoptions="${wgetoptions} -6" else wgetoptions="${wgetoptions} -4" fi local cmd="${WGET} ${wgetoptions} -O - ${ipurl}" if [ "${useIPv6}" = "enabled" ];then wanip=$($cmd | tr A-F a-f | sed s/[^0-9a-f:]//g) else wanip=$($cmd | sed s/[^0-9.]//g) fi [ -z "${wanip}" ] && wanip=${NOIP} else # no WAN IP found because of missing check URL wanip=${NOIP} fi } # actualy do the update (using wget or ez-ipupdate or even both) # this function is called via cronjob doUpdate() { if [ "${useIPv6}" = "enabled" ];then local dnsentry="$(host -t AAAA "${host}" | sed 's/.*address\s*//')" else local dnsentry="$(host -t A "${host}" | sed 's/.*address\s*//')" fi if [ "${dnsentry}" = "${wanip}" ];then return fi if [ ! -z "${server}" ];then start-stop-daemon -S -x "${UPDATE_TOOL}" -m -p "${PIDFILE}" -- -c "${cfgfile}" fi if [ ! -z "${updateurl}" ];then doReplaceVars if [ "${basicauth}" = "enabled" ];then WGETOPTIONS="${WGETOPTIONS} --user ${user} --password ${pass} " fi if [ "${ignoreCertError}" = "enabled" ];then WGETOPTIONS="${WGETOPTIONS} --no-check-certificate " fi if [ "${useIPv6}" = "enabled" ];then WGETOPTIONS="${WGETOPTIONS} -6" else WGETOPTIONS="${WGETOPTIONS} -4" fi local cmd="${WGET} -O /dev/null ${WGETOPTIONS} ${updateurl}" $cmd # ToDo: check the returning text from WEB Server. User need to give expected string. if [ ${?} -eq 0 ];then ${0} success ${wanip} else ${0} failed fi fi } umask u=rw,g=,o= cmd=${1} shift # decide which config to use cfgfile="/tmp/none" [ -f ${CFG_disabled} ] && cfgfile=${CFG_disabled} [ -f ${CFG} ] && cfgfile=${CFG} # check what action is requested case ${cmd} in configure) doGetOpt "${@}" doWriteCFG ;; start) doGetWANIP if [ "$(grep ^NAT ${HELPERCFG} | awk '{print $2}')" = "no" ];then #if we are not behind a NAT device and we use gnudip, start the daemon tool gnudipServer=$(grep ^server= ${cfgfile} 2> /dev/null | cut -d = -f 2- |grep -v ^\'\') if [ ! -f ${CFG} -a ! -z "${gnudipServer}" ];then mv ${CFG_disabled} ${CFG} /etc/init.d/${TOOLNAME} start fi # if we are not behind a NAT device and we use update-URL, add a cronjob # (daemon tool does not support update-URL feature) if [ ! -z "$(grep ^POSTURL $HELPERCFG | awk '{print $2}')" ];then echo "*/${UPDATEMINUTES} * * * * root ${0} update" > ${CRONJOB} $0 update fi else # if we are behind a NAT device, add a cronjob (daemon tool cannot monitor WAN IP changes) echo "*/${UPDATEMINUTES} * * * * root ${0} update" > $CRONJOB $0 update fi ;; get-nat) NAT=$(grep ^NAT $HELPERCFG 2> /dev/null | awk '{print $2}') [ -z "${NAT}" ] && NAT="unknown" echo ${NAT} ;; update) doReadCFG dnsentry=$(nslookup "${host}"|tail -n2|grep A|sed s/[^0-9.]//g) doGetWANIP echo ${wanip} > ${IPFILE} grep -v execute ${cfgfile} > ${cfgfile}.tmp mv ${cfgfile}.tmp ${cfgfile} echo "execute=${0} success ${wanip}" >> ${cfgfile} # if we know our WAN IP, only update if IP changes if [ "${dnsentry}" != "${wanip}" -a "${wanip}" != ${NOIP} ];then doUpdate else # If nothing has changed, nothing needs to be done but we # need to write the success status (if no success was # recorded yet because maybe DNS record was up to date # when script is executed for the first time) ${0} success ${wanip} fi # if we don't know our WAN IP do a blind update once a hour if [ "${wanip}" = ${NOIP} ];then currenttime=$(date +%s) LAST=0 [ -f ${LASTUPDATE} ] && LAST=$(cat ${LASTUPDATE}) diff=$((currenttime - LAST)) if [ ${diff} -gt ${UPDATEMINUTESUNKNOWN} ];then doUpdate fi fi ;; stop) rm ${CRONJOB} 2> /dev/null /etc/init.d/${TOOLNAME} stop kill "$(cat ${PIDFILE})" 2> /dev/null mv ${CFG} ${CFG_disabled} ;; success) date=$(date) echo "DNS record is up to date (${date})" > ${STATUSFILE} date +%s > ${LASTUPDATE} # if called from cronjob, the current IP is given as parameter if [ $# -eq 1 ];then echo "${1}" > ${IPFILE} else # if called from ez-ipupdate daemon, no WAN IP is given as parameter doGetWANIP echo ${wanip} > ${IPFILE} fi ;; failed) date=$(date) echo "last update failed (${date})" > ${STATUSFILE} ;; get-last-success) if [ -f ${STATUSFILE} ];then cat ${STATUSFILE} else echo "no successful update recorded since last config change" fi ;; status) doReadCFG doStatus ;; get-timer) echo ${UPDATEMINUTES} ;; clean) rm ${CFGDIR}/* rm ${CRONJOB} ;; *) echo "usage: status|configure |start|stop|update|get-nat|clean|success [updated IP]|failed|get-last-success" echo "" echo "options are:" echo "-s Gnudip Server address" echo "-d Domain to be updated" echo "-u Account username" echo "-P Account password" echo "-p Read Account Password from stdin" echo "-I A URL which returns the IP of the client who is requesting" echo "-U The update URL (a HTTP GET on this URL will be done)" echo "-c <1|0> disable SSL check on Update URL" echo "-b <1|0> use HTTP basic auth on Update URL" echo "-6 use IPv6 type address" echo "" echo "update do a one time update" echo "clean delete configuration" echo "success store update success and optional the updated IP" echo "failed store update failure" echo "get-nat return the detected nat type" echo "get-last-success return date of last successful update" ;; esac exit 0