159 lines
5.1 KiB
Bash
159 lines
5.1 KiB
Bash
#!/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
|