#!/bin/sh # JustASRV DDNS updater for Linux (POSIX sh + curl). https://docs.justasrv.com/linux # # Runs once per invocation; a systemd timer (or cron) calls it every 2 minutes. # - checks the public address with one small HTTPS request # - sends an update only when the address changed, or every REFRESH seconds as a heartbeat # (the heartbeat is a "nochg" on the server: no DNS write, but the host stays ONLINE) # - exponential backoff with jitter after failures; state survives reboots # - the token never appears on a command line (passed to curl on stdin) # - config is re-read every run, so a rotated token takes effect on the next run set -u VERSION=1.0.0 CONF=${JUSTASRV_CONF:-/etc/justasrv/justasrv.conf} STATE_DIR=${JUSTASRV_STATE:-/var/lib/justasrv} SERVER=https://ddns.justasrv.com REFRESH=600 HOSTNAME_FQDN= TOKEN= log() { # level message logger -t justasrv -p "user.$1" -- "$2" 2>/dev/null || true [ -t 1 ] && echo "justasrv: $2" return 0 } [ -r "$CONF" ] || { log err "config $CONF not readable"; exit 1; } # shellcheck disable=SC1090 . "$CONF" [ -n "$HOSTNAME_FQDN" ] && [ -n "$TOKEN" ] || { log err "HOSTNAME_FQDN and TOKEN must be set in $CONF"; exit 1; } command -v curl >/dev/null 2>&1 || { log err "curl is required"; exit 1; } mkdir -p "$STATE_DIR" && chmod 700 "$STATE_DIR" STATE="$STATE_DIR/state" LAST_IP= LAST_OK=0 FAILS=0 NEXT_TRY=0 [ -r "$STATE" ] && . "$STATE" NOW=$(date +%s) save() { umask 077 printf 'LAST_IP=%s\nLAST_OK=%s\nFAILS=%s\nNEXT_TRY=%s\n' "$LAST_IP" "$LAST_OK" "$FAILS" "$NEXT_TRY" > "$STATE.tmp" && mv "$STATE.tmp" "$STATE" } backoff() { # base_seconds reason FAILS=$((FAILS + 1)) n=$FAILS; [ "$n" -gt 6 ] && n=6 wait=$(( $1 * (1 << (n - 1)) )); [ "$wait" -gt 3600 ] && wait=3600 jitter=$(( $(od -An -N2 -tu2 /dev/urandom | tr -d ' ') % 60 )) NEXT_TRY=$((NOW + wait + jitter)) log warning "$2; retry in $((wait + jitter))s (failure $FAILS)" save exit 0 } [ "$NOW" -lt "$NEXT_TRY" ] && exit 0 # still backing off UA="JustASRV-Linux/$VERSION" CURL="curl -fsS --proto =https --tlsv1.2 --max-time 20 --retry 0 -A $UA" IP=$($CURL "$SERVER/ip" 2>/dev/null | tr -d '\r\n ') case "$IP" in *.*.*.*) ;; *) backoff 60 "cannot reach $SERVER (network down?)";; esac if [ "$IP" = "$LAST_IP" ] && [ $((NOW - LAST_OK)) -lt "$REFRESH" ]; then exit 0 # nothing changed, heartbeat not due fi # -K - reads options from stdin: credentials never show in `ps`. BODY=$(printf 'user = "%s:%s"\n' "$HOSTNAME_FQDN" "$TOKEN" | \ curl -sS --proto =https --tlsv1.2 --max-time 30 -A "$UA" -K - -w '\n%{http_code}' \ "$SERVER/nic/update?hostname=$HOSTNAME_FQDN" 2>/dev/null) CODE=$(printf '%s' "$BODY" | tail -n1) RESULT=$(printf '%s' "$BODY" | head -n1 | cut -d' ' -f1) case "$RESULT" in good|nochg) [ "$RESULT" = good ] && log notice "$HOSTNAME_FQDN updated to $IP" || { [ "$IP" != "$LAST_IP" ] && log info "$HOSTNAME_FQDN already $IP"; } LAST_IP=$IP LAST_OK=$NOW FAILS=0 NEXT_TRY=0 save ;; badauth) backoff 1800 "token rejected (revoked or rotated?) - update TOKEN in $CONF" ;; nohost) backoff 1800 "server says $HOSTNAME_FQDN is not valid for this token or is disabled" ;; abuse) backoff 900 "rate limited by server" ;; badip) backoff 1800 "server rejected the address $IP" ;; dnserr|911) backoff 120 "server-side DNS error (change is queued server-side)" ;; *) backoff 60 "update failed (HTTP ${CODE:-none}): $(printf '%s' "$BODY" | head -c 80)" ;; esac