#!/bin/sh #------------------------------------------------------------------------------ # /usr/bin/dyndns-update.sh - Update one DynDNS-provider # part of Package DYNDNS __FLI4LVER__, see documentation for licence # # © Copyright 2003-2004 Tobias Gruetzmacher # (c) copyright 2014- babel (Claas Hilbrecht) # # Creation: 16.09.2003 tobig # Last Update: $Id$ #------------------------------------------------------------------------------ . /etc/rc.d/circuit-common LOG4SH_CONFIGURATION="/etc/log4sh/dyndns.conf" . /usr/share/log4sh # # Error codes # # connection related errors ECONN_CONN=1 ECONN_PERM=2 # # provider related errors # EPROV_TMP=11 EPROV_PERM=12 EPROV_EMPTY=13 EPROV_UNKNOWN=14 # # packet related errors # EPACK_NOPATTERN=21 EPACK_INVFUNC=22 # # try related errors ETRY_TMP=31 ETRY_PERM=32 ETRY_NOMETHOD=33 # # # ECONF_MISSING=41 # checkip host to use, will be configurable some day checkip_host="http://checkip.dyndns.org" cleanup_and_exit () { local exitcode=$1 _log4sh_cleanup EXIT exit $exitcode } abort () { log_error "$1" cleanup_and_exit 1 } usage () { logger_info "Usage:" logger_info " update [ ]" logger_info " enable " logger_info " enable_method " logger_info " cancel_update " logger_info " status " logger_info "Additional options:" logger_info " -x : generate trace in /var/tmp/dyndns-*" cleanup_and_exit 1 } comment () { echo "# DynDNS-Comment: $1" } dyndns_date () { date } run_curl () { local cert= if echo "$update_host" | grep -q "^https://" then cert="--insecure --tlsv1" eval ca_file='$update_host_'$idx'_ca' if [ -n "$ca_file" ] then if [ -f "/etc/ssl/$ca_file" ] then cert="--cacert /etc/ssl/$ca_file" elif [ -f "/usr/share/curl/$ca_file" ] then cert="--cacert /usr/share/curl/$ca_file" fi fi fi local cmd="curl $cert --globoff --silent --no-buffer \ --show-error --stderr - --output - \ --user-agent \"dyndns_fli4l/$dyn_ver\" \ --anyauth --user \"$login_username:$login_password\" \ --interface $circ_dev_ip --ipv4 \ --connect-timeout $dyn_timeout \ \"${update_host}\"" #logger_debug "$cmd" eval "$cmd" case $? in 0) return 0 ;; 7 | 28) # temporary failure, other methods still might work # 7 - Failed to connect to host. # 28 - Operation timeout. The specified time-out period # was reached according to the conditions. return $ECONN_CONN ;; *) return $ECONN_PERM ;; esac } # # This is a plain http-request with or without basic auth # http_update () { for idx in `seq 1 $update_host_n` do eval update_host='$update_host_'$idx run_curl return $? done return $ECONN_CONN } # # Use GnuDip over HTTP (Ask me if you need the direct socket connection) # # http://gnudip2.sourceforge.net/gnudip-www/latest/gnudip/html/protocol.html # gnudiphttp_update () { for idx in `seq 1 $update_host_n` do eval update_host='$update_host_'$idx tmpfile="/tmp/dyndns.$$.gnudip" { run_curl }>$tmpfile if [ $? -ne 0 ] then rm -f $tmpfile return $ECONN_CONN fi # # Sets meta_salt, meta_time and meta_sign # eval "`sed -n 's/^.*$/meta_\1='"'\2'"'/; /^meta_/ p' $tmpfile`" rm -f $tmpfile set -- `echo -n "${dyndns_md5password}.${meta_salt}" | md5sum` endpass="$1" update_host="$update_host/?salt=${meta_salt}&time=${meta_time}&sign=${meta_sign}&user=${dyndns_username}&pass=${endpass}&domn=${dyndns_domain}&reqc=2" run_curl return $? done } netcat_update () { for port in $provider_port do if echo -e "$provider_data" |\ netcat -s $circ_dev_ip -w "$dyn_timeout" "$provider_host" "$port" then return 0 fi done return $ECONN_CONN } nsupdate_update () { logger_setThreadName "nsupdate" for idx in `seq 1 $update_host_n` do eval update_host='$update_host_'$idx #logger_info "Trying update host '$update_host' with nsupdate method" # split ip/net and port set `echo $update_host | sed 's/:/ /'` update_host=$1 port=$2 # work around a problem with IPv4 and IPv6 host. nsupdate seems to try to reach # the update_host with IPv6 first if this is avaiable local ns_ipv4=`dig -4 +noall +short $update_host` local cmd=$( echo "" echo "server $ns_ipv4 $port" echo "local $circ_dev_ip" if [ "$update_a" = "yes" ] then echo "update delete $dyndns_fqdn_hostname A" echo "update add $dyndns_fqdn_hostname $update_a_ttl A ${dyndns_new_ipv4}" fi if [ "$update_aaaa" = "yes" ] then echo "update delete $dyndns_fqdn_hostname AAAA" echo "update add $dyndns_fqdn_hostname $update_aaaa_ttl AAAA ${dyndns_new_ipv6}" fi if [ "$update_wilcard" = "yes" ] then echo "update add *.$dyndns_fqdn_hostname $update_wilcard_ttl CNAME $dyndns_fqdn_hostname" fi #[ "$update_mx" = "yes" ] && "update add $hostname $update_mx_ttl mx" echo "key $dyndns_username $dyndns_password" echo "show" echo "send" echo "answer" echo "quit" ) logger_debug "nsupdate called with:$cmd" IFS='' res=`echo $cmd | nsupdate` unset IFS logger_debug "$res" done logger_setThreadName "main" return $EPROV_UNKNOWN } decode_file () { echo >> $1 if grep -q '^Transfer-Encoding: chunked' $1 then sed -e '${/^[[:space:]]*$/d}' $1 | decode_chunked else sed -e '${/^[[:space:]]*$/d}' $1 fi } grep_meta_info () { sed -n '//\>/g p }' } strip_html_header () { sed -e "/<.DOCTYPE/!bb :a //!{ N ba } s:\(<.DOCTYPE.*\):: :b /<\/body/!bd :c /<\/html>/!{ N bc } s:\(.*\):: :d /^Content-Type.*/r $1" } keep_copy () { local arch=$dyn_logdir/archive local arch_copy=$arch/${1##*/}.`date +%d.%m.%Y` [ -d $arch ] || mkdir -p $arch cp $1 $arch_copy comment "Can not decide, whether this update was ok or not - considering it as ok." comment "Success: '$success', Failure: '$failure'" echo "# DynDNS-Report: $arch_copy" } try_method () { local sim= echo "# DynDNS-Method: '$update_method'" echo -n "# DynDNS-Date: " dyndns_date eval sim=\$provider_${update_method}_sim_error [ "$sim" ] && return $sim res= # # contact provider # { ${update_method}_update res=$? } > $dyn_trylog 2> $dyn_trylog.err echo "----- start answer -----" if grep -q "Content-Type:.*text/html" $dyn_trylog then sed_tmp=/tmp/dyndns.sed.$$ grep_meta_info < $dyn_trylog > $sed_tmp decode_file $dyn_trylog | strip_html_header $sed_tmp rm -f $sed_tmp else decode_file $dyn_trylog fi echo "------ end answer ------" # if there's any content from stderr if [ -s $dyn_trylog.err ] then echo "----- start error -----" cat $dyn_trylog.err echo "------ end error ------" #if grep -q "$update_failure" $dyn_trylog.err #then # res=$EPROV_PERM #fi fi res=0 if [ $res -eq 0 ] then if grep -q -v '^[[:space:]]*$' $dyn_trylog then # # check for success # case "$update_success" in no_check) # # do not check at all # res=$EPROV_UNKNOWN ;; none) # # success pattern unknown, only check failure pattern # if grep -q "$update_failure" $dyn_trylog then res=$EPROV_PERM else res=$EPROV_UNKNOWN fi ;; '|'*) # # complex check, invoke check function # ${update_success#|} $dyn_trylog res=$? ;; *) # # success and failure pattern available, check success first, then failure # if grep -q "$update_success" $dyn_trylog then res=0 elif grep -q "$update_failure" $dyn_trylog then res=$EPROV_PERM else res=$EPROV_UNKNOWN fi ;; esac else res=$EPROV_EMPTY fi fi return $res } disable_method () { update_method=`echo "$update_method" | sed -e "s/$1\([[:space:]]\+\|$\)//g"` } try_update () { if [ -f $dyn_disabled.$method ] then comment "All methods disabled, no method left to try." disable_method $method return $ETRY_NOMETHOD fi while true do try_method # errors # ECONN_CONN, ECONN_PERM, # EPROV_TMP, EPROV_PERM, EPROV_EMPTY, EPROV_UNKNOWN # EPACK_NOPATTERN, EPACK_INVFUNC case $? in 0) return 0 ;; $ECONN_CONN) # connection problem - try one of the other methods comment 'Failed to connect provider.' break ;; $ECONN_PERM) # connection problem, disable method comment "Permanent connection error, disabling method '$update_method'." cp $dyn_trylog.err $dyn_disabled.$method disable_method $method break ;; $EPROV_TMP) # temporary problem, try again after some time comment "Temporary problem, re-trying again after timeout (${dyn_method_timeout}s)." sleep $dyn_method_timeout continue ;; $EPROV_PERM) # permanent error, stop updating account, until # the problem is fixed comment "Permanent error, disabling host '$dyndns_fqdn_hostname'." cp $dyn_trylog.err $dyn_disabled return $ETRY_PERM ;; $EPROV_EMPTY) # empty reply from provider, try other method comment 'Empty reply from provider.' break ;; $EPROV_UNKNOWN) # can not decide whether this was ok or not, consider this to be ok keep_copy $dyn_trylog return 0 ;; $EPACK_NOPATTERN | $EPACK_INVFUNC) # packet problem, disable method, try another one comment "Packet error, check pattern or check function missing or broken, disabling method '$update_method'." cp $dyn_trylog.err $dyn_disabled.$method disable_method $method break ;; *) # permanent error, stop updating account, until # the problem is fixed comment 'Unkown error, disabling host.' cp $dyn_trylog.err $dyn_disabled return $ETRY_PERM ;; esac done if [ ! "$update_method" ] then comment "All methods disabled, no method left to try." return $ETRY_NOMETHOD fi return $ETRY_TMP } aquire_lock () { # # update already running? # if [ -f $dyn_pid_file ] then read pid < $dyn_pid_file if kill -0 $pid 2> /dev/null then # kill whole process group logger_error "Update still running (pid $pid), terminating previous update ..." pg=`cut -d' ' -f 5 /proc/$pid/stat` [ "$pg" ] && kill -TERM -$pg 2> /dev/null else logger_warn "ignoring stale pid file (pid $pid) ..." fi fi echo $$ > $dyn_pid_file } generic_detect_ext_ipv4 () { dyndns_new_ipv4= case "$detect_ext_ipv4" in none) dyndns_new_ipv4=$circ_dev_ip ;; checkip) dyndns_new_ipv4=`curl --silent --globoff --interface $circ_dev_ip --ipv4 --connect-timeout $dyn_timeout ${checkip_host} | sed -n -e 's/.*Current IP Address:[[:space:]]*\([0-9.]\+\).*/\1/p'` ;; stun) dyndns_new_ipv4=`get_ext_ip_via_stun $circ_dev_ip` ;; *) abort "Unknown DYNDNS_x_DETECT_EXT_IPV4 setting <$detect_ext_ipv4>!" esac if [ -n "$dyndns_new_ipv4" ] then set $dyndns_new_ipv4 [ $# -eq 1 ] || abort "Interface '$circ_dev' of circuit '$circuit' has multiple IPv4 addresses ($*). Don't know which one should be used to update the host!" else abort "Unable to determine IPv4 address of interface '$circ_dev' for circuit '$circuit'!" fi logger_info "Using IPv4 address ${dyndns_new_ipv4} determined with method '${detect_ext_ipv4}' via device '${circ_dev} (${circ_dev_ip})' for update." } generic_check_with_ipv4_nslookup () { set -- `nslookup $dyndns_fqdn_hostname | sed -n -e '/^Server:/,/^Address/d;s/^Address[^:]*://p'` dyndns_old_ipv4=$1 } generic_check_with_ipv4_dig () { dyndns_old_ipv4=`dig -4 +noall +short $dyndns_fqdn_hostname` } generic_check_with_ipv4_dig_authoritative () { dyndns_old_ipv4= authoritative_ns=`dig -4 +noall +authority $dyndns_fqdn_hostname NS|sed -e 's/\(^[^[:space:]]\+\).*/\1/'` if [ -n "$authoritative_ns" ] then authoritative_ns=`dig -4 +noall +answer +short $authoritative_ns NS|shuf|head -n 1` if [ -n "$authoritative_ns" ] then logger_info "Using authorative nameserver $authoritative_ns to lookup current IPv4 address." dyndns_old_ipv4=`dig -4 +noall +short @$authoritative_ns $dyndns_fqdn_hostname` else abort "No usable authorative nameserver found, give up!" fi else abort "No usable authorative nameserver found, give up!" fi } check_dyn_runfile () { dyndns_old_ipv4= if [ -f $confdir/lookup_names ] then eval local get_ip=\$${provider}_get_ip : ${get_ip:=generic_check_with_ipv4_nslookup} $get_ip fi echo -e "dyndns_old_ipv4='$dyndns_old_ipv4'\nregistered_date='`date +%s`'\nregistered_checked_only=yes" > $dyn_runfile } generic_check_with_ipv4_cache () { dyndns_old_ipv4= [ -f $dyn_runfile ] || check_dyn_runfile [ -f $dyn_runfile ] || return . $dyn_runfile if [ "$dyndns_new_ipv4" = "$dyndns_old_ipv4" ] then time=`date +%s` if [ $time -gt $registered_date ] then time_diff=`expr $time - $registered_date` # # make renew time configurable # if [ "$dyn_force" = "yes" ] then : ${dns_refresh:=29} if echo "$dns_refresh" | grep -q "^[[:digit:]]\+$" then renew_time=`expr $dns_refresh \* 86400` # 86400 seconds per day if [ $time_diff -ge $renew_time ] then logger_info "Forcing update, last update was more than $dns_refresh days ago." do_update=yes fi fi else renew_time=1800 # 30 min if [ $time_diff -lt $renew_time ] then logger_warning "Ignoring forced update, minimum wait time isn't over yet." else do_update=yes fi fi fi else do_update=yes fi } check_update_needed () { # the previous/old IPv4 is already fetched now try to compare with the current IPv4 address case "$check_with" in # we simply use our internal IP address cache file none) logger_info "IPv4 change address check is disabled with DYNDNS_x_CHECK_WITH='none'!" return ;; cache) generic_check_with_ipv4_cache ;; # we do a simple nslookup nslookup) generic_check_with_ipv4_nslookup ;; # we use dig to query the standard DNS server dig) generic_check_with_ipv4_dig ;; # we use dig to query the authoritative DNS server (automatically determined) dig-query-authority) generic_check_with_ipv4_dig_authoritative ;; esac if [ -z "$dyndns_old_ipv4" ] then logger_error "Failed to get current IPv4 address via ${check_with}, giving up!" cleanup_and_exit $ETRY_TMP fi if [ "$dyndns_new_ipv4" = "$dyndns_old_ipv4" ] then logger_info "No update needed, IPv4 address (detected: ${dyndns_new_ipv4} via ${detect_ext_ipv4} / DNS: ${dyndns_old_ipv4}) didn't changed." if [ ! -f $dyn_runfile ] then logger_info "No previous dyndns result is available, creating fake result file for webgui." echo "registered_ipv4=${dyndns_new_ipv4}" >$dyn_runfile echo "registered_date=`dyndns_date`" >>$dyn_runfile else rm -f $dyn_pid_file cleanup_and_exit 0 fi fi } do_update () { # # account disabled? # [ -f $dyn_disabled ] && abort "Disabled until problem is fixed, see log for mor details ($dyn_disabled or dyndns web interface)!" aquire_lock # # get external ipv4 # generic_detect_ext_ipv4 if [ "$force_update" != "yes" ] then # if no update is nessary this check never return but exits the update script check_update_needed fi while true do { echo "# DynDNS-Provider: $provider" echo "# DynDNS-Forced-Update: $force_update" echo "# DynDNS-Registered-IP: $dyndns_new_ipv4" echo -n "# DynDNS-Begin: " dyndns_date echo -e "# DynDNS-End: unfinished\n# DynDNS-Result: unknown" try_update res=$? [ $res -eq $ETRY_TMP ] && comment "All methods failed, restarting after timeout (${dyn_retry_timeout}s)." } > $dyn_logfile if [ $res -eq 0 ] then res_txt=Success else res_txt=Failed fi sed -i -e "s/\(# DynDNS-End: \).*/\1`dyndns_date`/;s/\(# DynDNS-Result: \).*/\1$res_txt/" $dyn_logfile cat $dyn_logfile >> $dyn_history chmod a+r $dyn_logfile if [ "$dyn_lock" ] then if [ -f "$dyn_lock" ] then rm -f $dyn_lock dyndns_log "released lock" else [ "$dyn_clear_log" ] && logger_warn "lock already released by someone else" fi dyn_clear_log= fi case $res in $ETRY_TMP) sleep $dyn_retry_timeout continue ;; 0) echo -e "registered_ipv4='$dyndns_new_ipv4'\nregistered_date='`date +%s`'" > $dyn_runfile logger_info "Update successful" ;; $ETRY_PERM) logger_error "Provider signaled an error, update failed!" ;; $ETRY_NOMETHOD) logger_error "No methods left to try, update failed!" ;; *) logger_error "Unknown error '$res', update failed!" ;; esac rm -f $dyn_trylog $dyn_trylog.err $dyn_pid_file cleanup_and_exit $res done } cancel_update () { if [ -f $dyn_pid_file ] then read pid < $dyn_pid_file rm -f $dyn_pid_file if kill -0 $pid 2> /dev/null then children=`sed -n -e "/^Pid:/h;/^PPid:.*$pid/{g;s/^Pid:[[:space:]]*//p}" /proc/*/status` if kill $pid 2> /dev/null then # # terminate any pending child processes too # kill $children str="# DynDNS-Cancel: `dyndns_date`" echo "$str" >> $dyn_logfile cat $dyn_logfile >> $dyn_history return 0 else logger_error "failed to terminate update (pid: $pid)!" fi fi fi logger_info "No running update found." return 1 } enable_host() { if [ -f $dyn_disabled ] then rm -f $dyn_disabled set $service_update_method if [ $# -eq 1 ] then [ -f $dyn_disabled.$method ] && rm -f $dyn_disabled.$method fi str="# DynDNS-Enable: `dyndns_date`" echo "$str" >> $dyn_history echo "$str" >> $dyn_logfile fi return 0 } enable_method() { method=$ip [ -f $dyn_disabled.$method ] && rm -f $dyn_disabled.$method } do_status () { if [ -f $dyn_disabled ] then echo "$dyndns_fqdn_hostname: disabled" else if [ -f $dyn_runfile.pid ] then read pid < $dyn_runfile.pid echo "$dyndns_fqdn_hostname: update running ($pid)" else if [ -f $dyn_logfile ] then echo "$dyndns_fqdn_hostname: ok" else echo "$dyndns_fqdn_hostname: not updated yet" fi fi fi } set_defaults () { # # set default values # confdir="/etc/dyndns" dyn_ver="4.0.0-trunk-x86-r48178" dyn_logdir="/var/log/dyndns" dyn_histdir="${dyn_logdir}/history" dyn_rundir="/var/run/dyndns" dyn_logfile="$dyn_logdir/$dyndns_fqdn_hostname" dyn_runfile="$dyn_rundir/$dyndns_fqdn_hostname" dyn_history="$dyn_histdir/$dyndns_fqdn_hostname" dyn_disabled="${dyn_runfile}.disabled" dyn_pid_file="$dyn_rundir/$dyndns_fqdn_hostname.pid" dyn_timeout="60" dyn_method_timeout=1800 dyn_retry_timeout=1800 dyn_trylog=/tmp/dyndns-update.$$.$dyndns_fqdn_hostname dyn_clear_log=yes } get_dyndns_new_ipv4_for_update () { generic_detect_ext_ipv4 } get_config () { # # get config for hostname # if [ ! -f $confdir/host.d/$dyndns_fqdn_hostname.conf ] then logger_fatal "The configuration for the host $dyndns_fqdn_hostname is missing!" cleanup_and_exit $ECONF_MISSING fi . $confdir/host.d/$dyndns_fqdn_hostname.conf # no circuit is given, trying to find suitable circuit if [ -z "$circuit" ] then logger_info "No circuit is given, trying to find active circuit to update host." for idx in `seq 1 $circuit_n` do eval circ='$circuit_'$idx for c in $(circuit_resolve $circ) do # hack multiple concurrent online circuits #if [ `ip rule show | wc -l` > 3 ] #then # circuit=$c # circuit_read_field $circuit circ_name # logger_info "The circuit '$circ_name' (ID: '$circuit') seems to be online, using this circuit to update the host." # break #fi # TODO: implemented support for updates through IPv6 if circuit_is_l3prot_up $c ipv4 then circuit=$c circuit_read_field $circuit circ_name logger_info "The circuit '$circ_name' (ID: '$circuit') seems to be online, using this circuit to update the host." break fi done [ -n "$circuit" ] && break done if [ -z "$circuit" ] then abort "There's no circuit online matching name, alias or class $circ!" fi fi # maybe this is an alias, try resolve alias first circuit_resolve_alias circuit # get device used by this circuit circuit_read_field $circuit circ_dev if [ -z "$circ_dev" ] then abort "Can't get circuit ID for circuit <$circuit>!" fi # get IP of our outgoing WAN device to use this for source routing with netcat and STUN circ_dev_ip=`ip a s $circ_dev | sed -ne 's/^[[:space:]]*inet[[:space:]]\([0-9\.]*\).*/\1/p'` if [ -z "$circ_dev_ip" ] then abort "Can't get IPv4 address of interface '${circ_dev}!" fi [ -f $dyn_rundir/inv_user ] && user="inv_$dyndns_username" [ -f $dyn_rundir/inv_pass ] && user="inv_$dyndns_password" [ -f $dyn_rundir/inv_host ] && user="inv_$dyndns_fqdn_hostname" # # read config for provider # . $confdir/provider.d/$provider [ "$update_method" ] || abort "Empty update type in $confdir/provider.d/$provider!" } # # check parameters # prog=${0##*/} force_update="no" logger_debug "Starting dyndns-update script..." action=update args=2 while [ "$1" ] do case "$1" in -x | -vx) deb_flag=$1 ;; -f) force_update="yes" logger_info "DynDNS update forced, please be careful to not get banned!" ;; -l) if [ "$2" ] then dyn_lock=$2 shift else usage fi ;; -s) syslog=yes ;; update | enable_method) action=$1 args=2 ;; enable | cancel | status) action=$1 args=1 ;; *) break ;; esac shift done if [ $# -ne $args ] then [ $action != update -o $# -ne 1 ] && usage fi if [ "$deb_flag" ] then file=/var/tmp/dyndns-update_`date +%y-%m-%d_%H-%M-%S`_${action}_${1}_trace.log exec 2> $file set $deb_flag fi dyndns_fqdn_hostname="$1" circuit="$2" tmpmsg="$2" : ${tmpmsg:="autodetect"} logger_info "Using '${dyndns_fqdn_hostname}' as the hostname to work with command '${action}' and the circuit '${tmpmsg}'" # # divide hostname in hostname and domain # dyndns_hostname=${dyndns_fqdn_hostname%.*.*} dyndns_domain=${dyndns_fqdn_hostname#$dyndns_hostname.} set_defaults get_config # # create md5 password # set -- `echo -n "$dyndns_password" | md5sum` dyndns_md5password="$1" case $action in update) do_update ;; enable) enable_host ;; enable_method) enable_method ;; cancel) cancel_update ;; status) do_status ;; esac cleanup_and_exit $?