#!/system/bin/sh # Android DDNS reporter # # Reads the current IPv4 address of $IFACE and GETs the Cloudflare Worker # endpoint whenever the IP changes. Uses the system-provided httpurl binary # (HTTP-only, no curl/wget/busybox needed). Token and IP are passed as # query-string parameters. # # Configure via environment variables or by editing the constants below. # Defaults are suitable for /data/local/tmp installation on an Android IPTV box. set -u # Load optional env file (created by install.sh). This is sourced *before* # the defaults below so values from the file act as overrides rather than # hard-coded constants. DDNS_ENV_FILE="${DDNS_ENV_FILE:-/data/local/tmp/ddns.env}" if [ -f "$DDNS_ENV_FILE" ]; then # shellcheck disable=SC1090 . "$DDNS_ENV_FILE" fi # ---- Configuration (override by exporting env vars before calling) ------------ WORKER_URL="${DDNS_WORKER_URL:-http://ddns.eachtime.me/update}" TOKEN="${DDNS_TOKEN:-REPLACE_ME_SHARED_TOKEN}" NAME="${DDNS_NAME:-}" IFACE="${DDNS_IFACE:-eth0}" INTERVAL="${DDNS_INTERVAL:-60}" HTTPURL="${DDNS_HTTPURL:-/system/xbin/httpurl}" STATE_FILE="${DDNS_STATE_FILE:-/data/local/tmp/ddns.last}" LOG_FILE="${DDNS_LOG_FILE:-/data/local/tmp/ddns.log}" MAX_LOG_BYTES="${DDNS_MAX_LOG_BYTES:-524288}" # 512 KiB rotation threshold BACKOFF_MAX="${DDNS_BACKOFF_MAX:-600}" # max backoff seconds on failure ONESHOT="${DDNS_ONESHOT:-0}" # 1 = run once and exit # ------------------------------------------------------------------------------ DATE_CMD="/system/bin/toybox date" log() { # Log with timestamp; rotate if the log exceeds MAX_LOG_BYTES. ts=$($DATE_CMD '+%Y-%m-%d %H:%M:%S') printf '%s %s\n' "$ts" "$*" >>"$LOG_FILE" 2>/dev/null if [ -f "$LOG_FILE" ]; then size=$(/system/bin/toybox stat -c '%s' "$LOG_FILE" 2>/dev/null || echo 0) if [ "${size:-0}" -gt "$MAX_LOG_BYTES" ] 2>/dev/null; then mv "$LOG_FILE" "${LOG_FILE}.1" 2>/dev/null fi fi } get_ip() { # Auto-detect outbound source IP via routing table (no awk needed). /system/bin/ip -4 route get 8.8.8.8 2>/dev/null \ | /system/bin/toybox sed -n 's/.*src \([0-9.]*\).*/\1/p' \ | /system/bin/toybox head -n1 } is_public_ipv4() { # Reject empty, private, CGNAT, link-local, loopback, multicast/reserved. case "$1" in ""|0.*|127.*|169.254.*|10.*|192.168.*) return 1 ;; 172.16.*|172.17.*|172.18.*|172.19.*|172.2[0-9].*|172.3[0-1].*) return 1 ;; 100.6[4-9].*|100.[7-9][0-9].*|100.1[0-1][0-9].*|100.12[0-7].*) return 1 ;; 22[4-9].*|23[0-9].*|24[0-9].*|25[0-5].*) return 1 ;; esac # Must match rough IPv4 shape (4 dotted numbers). case "$1" in *.*.*.*) return 0 ;; *) return 1 ;; esac } send_update() { # $1 = ip. Echoes the JSON response body on stdout. # Exit status: 0 if httpurl succeeded AND response contains "ok":true. ip="$1" url="${WORKER_URL}?t=${TOKEN}&ip=${ip}&name=${NAME}" # httpurl outputs diagnostic lines (Resolving/Connecting) then HTTP headers # then a blank line then the response body, all on stdout. raw=$("$HTTPURL" "$url" 2>&1) || { echo "$raw"; return 1; } # Extract JSON body (first line that starts with '{'). body=$(echo "$raw" | grep '^{' | head -n1) echo "$body" # Check for success. echo "$body" | grep -q '"ok":true' } run_once() { ip=$(get_ip) if ! is_public_ipv4 "$ip"; then log "skip: no public ipv4 detected (got '$ip')" return 2 fi last="" [ -f "$STATE_FILE" ] && last=$(cat "$STATE_FILE" 2>/dev/null) if [ "$ip" = "$last" ]; then # No change, no request. Keep log quiet. return 0 fi resp=$(send_update "$ip" 2>&1) rc=$? if [ $rc -eq 0 ]; then printf '%s' "$ip" >"$STATE_FILE" log "ok: $last -> $ip :: $resp" return 0 else log "fail($rc): attempt $ip :: $resp" return 1 fi } main_loop() { backoff=0 trap 'log "stopping (signal)"; exit 0' TERM INT log "starting: iface=$IFACE interval=${INTERVAL}s url=$WORKER_URL" while :; do if run_once; then backoff=0 sleep "$INTERVAL" else # Exponential backoff capped at BACKOFF_MAX. if [ "$backoff" -eq 0 ]; then backoff="$INTERVAL" else backoff=$((backoff * 2)) [ "$backoff" -gt "$BACKOFF_MAX" ] && backoff="$BACKOFF_MAX" fi log "retry in ${backoff}s" sleep "$backoff" fi done } # Sanity checks if [ ! -x "$HTTPURL" ]; then log "fatal: httpurl binary not found at $HTTPURL" exit 127 fi if [ "$TOKEN" = "REPLACE_ME_SHARED_TOKEN" ]; then log "fatal: DDNS_TOKEN is unset; edit this script or export DDNS_TOKEN" exit 2 fi if [ -z "$NAME" ]; then # Auto-derive from device hostname, truncated to first segment. NAME=$(cat /proc/sys/kernel/hostname 2>/dev/null | /system/bin/toybox cut -d_ -f1) [ -z "$NAME" ] && NAME="dd" log "auto name=$NAME (set DDNS_NAME to override)" fi if [ "$ONESHOT" = "1" ]; then run_once exit $? fi main_loop