#!/usr/bin/env bash # cf-ddns.sh - Cloudflare DDNS updater set -euo pipefail DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -f "${DIR}/.env" ]; then # shellcheck disable=SC1091 source "${DIR}/.env" else echo "$(date -Iseconds) ERROR: missing ${DIR}/.env (create from .env.example)" >&2 exit 1 fi : "${CF_API_TOKEN:?need CF_API_TOKEN in .env}" : "${ZONE:?need ZONE in .env}" : "${RECORDS:?need RECORDS in .env}" : "${IP_URL:=https://ipv4.icanhazip.com}" : "${PROXIED:=false}" AUTH_HEADER="Authorization: Bearer ${CF_API_TOKEN}" CT_HEADER="Content-Type: application/json" log() { echo "$(date -Iseconds) $*"; } IP=$(curl -fsS "${IP_URL}" | tr -d '[:space:]') if [[ -z "${IP}" ]]; then log "ERROR: could not determine public IP" exit 1 fi ZONE_ID=$(curl -fsS -X GET "https://api.cloudflare.com/client/v4/zones?name=${ZONE}" \ -H "${AUTH_HEADER}" -H "${CT_HEADER}" | jq -r '.result[0].id // empty') if [[ -z "${ZONE_ID}" ]]; then log "ERROR: zone not found: ${ZONE}" exit 1 fi IFS=',' read -r -a RECORD_ARR <<< "${RECORDS}" for fullrec in "${RECORD_ARR[@]}"; do rec=$(echo "${fullrec}" | xargs) log "Processing ${rec}" info=$(curl -fsS -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${rec}&type=A" \ -H "${AUTH_HEADER}" -H "${CT_HEADER}") RECORD_ID=$(echo "$info" | jq -r '.result[0].id // empty') CURRENT_CONTENT=$(echo "$info" | jq -r '.result[0].content // empty') if [[ -z "${RECORD_ID}" ]]; then log "Creating ${rec} -> ${IP}" create_resp=$(curl -fsS -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \ -H "${AUTH_HEADER}" -H "${CT_HEADER}" \ --data "{\"type\":\"A\",\"name\":\"${rec}\",\"content\":\"${IP}\",\"ttl\":1,\"proxied\":${PROXIED}}") ok=$(echo "$create_resp" | jq -r '.success // false') if [[ "$ok" != "true" ]]; then log "ERROR creating ${rec}: ${create_resp}" else log "Created ${rec}" fi else if [[ "${CURRENT_CONTENT}" == "${IP}" ]]; then log "${rec} unchanged (${IP})" else log "Updating ${rec} -> ${IP}" upd_resp=$(curl -fsS -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \ -H "${AUTH_HEADER}" -H "${CT_HEADER}" \ --data "{\"type\":\"A\",\"name\":\"${rec}\",\"content\":\"${IP}\",\"ttl\":1,\"proxied\":${PROXIED}}") ok=$(echo "$upd_resp" | jq -r '.success // false') if [[ "$ok" != "true" ]]; then log "ERROR updating ${rec}: ${upd_resp}" else log "Updated ${rec}" fi fi fi done