#!/usr/bin/env bash set -euo pipefail ENV_FILE="/etc/cf-ddns/cf-ddns.env" STATE_DIR="/var/lib/cf-ddns" API="https://api.cloudflare.com/client/v4" log() { logger -t cf-ddns "$*"; echo "[cf-ddns] $*"; } fail() { log "ERROR: $*"; exit 1; } # --- Load env --- [[ -f "$ENV_FILE" ]] || fail "Missing $ENV_FILE" # shellcheck disable=SC1090 source "$ENV_FILE" : "${CF_API_TOKEN:?CF_API_TOKEN required in env}" : "${CF_PROXIED:=false}" : "${CF_TTL:=120}" # --- Names from system --- CF_RECORD_NAME="$(hostname -f 2>/dev/null || true)" CF_SHORT_NAME="$(hostname -s 2>/dev/null || true)" CF_ZONE_NAME="$(hostname -d 2>/dev/null || true)" [[ -n "$CF_RECORD_NAME" ]] || fail "hostname -s returned empty" [[ -n "$CF_ZONE_NAME" ]] || fail "hostname -d returned empty (set a DNS/search domain)" # --- short_host(): derive short alias used for the CNAME --- short_host() { local short_hostname part1 part2 part3 part4 num_parts second_initial transformed_hostname short_hostname="$(hostname -s)" IFS="-" read -r part1 part2 part3 part4 <<< "$short_hostname" num_parts=$(awk -F'-' '{print NF}' <<< "$short_hostname") if [ "$num_parts" -eq 4 ]; then second_initial=${part2:0:1} # kept in case you want it later transformed_hostname="${part2}-${part4}" else transformed_hostname="$short_hostname" fi echo "$transformed_hostname" } SHORT_ALIAS="$(short_host)" SHORT_ALIAS_FQDN="${SHORT_ALIAS}.${CF_ZONE_NAME}" # Safe filename for state safe_name="$(echo "$CF_RECORD_NAME" | tr '/:' '__')" LAST_IP_FILE="${STATE_DIR}/last_ipv4_${safe_name}" mkdir -p "$STATE_DIR" # --- Detect local IPv4 on default route interface --- detect_ip() { local iface ip iface="$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')" if [[ -z "${iface}" ]]; then iface="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}')" fi [[ -n "$iface" ]] || fail "Could not detect default route interface" ip="$(ip -4 addr show dev "$iface" | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1)" [[ -n "$ip" ]] || fail "No IPv4 found on default interface '$iface'" echo "$ip" } current_ip="$(detect_ip)" previous_ip="$( [[ -f "$LAST_IP_FILE" ]] && cat "$LAST_IP_FILE" || echo "" )" if [[ "$current_ip" == "$previous_ip" && -n "$current_ip" ]]; then log "IP unchanged ($current_ip); no update needed for ${CF_RECORD_NAME}." exit 0 fi # --- Cloudflare helpers --- urlenc() { local s="$1"; s="${s// /%20}"; s="${s//\//%2F}"; echo -n "$s"; } hdr=(-H "Authorization: Bearer ${CF_API_TOKEN}" -H "Content-Type: application/json") # 1) Get Zone ID zone_q="name=$(urlenc "$CF_ZONE_NAME")" zone_id="$(curl -fsS "${hdr[@]}" "${API}/zones?${zone_q}" \ | sed -n 's/.*"id":"\([^"]*\)".*"name":"'"$CF_ZONE_NAME"'".*/\1/p' | head -n1)" [[ -n "$zone_id" ]] || fail "Could not find Zone ID for ${CF_ZONE_NAME}" # 2) Lookup existing A record by FQDN rec_q="type=A&name=$(urlenc "$CF_RECORD_NAME")" record_json="$(curl -fsS "${hdr[@]}" "${API}/zones/${zone_id}/dns_records?${rec_q}")" record_id="$(echo "$record_json" | sed -n 's/.*"id":"\([^"]*\)".*"name":"'"$CF_RECORD_NAME"'".*"type":"A".*/\1/p' | head -n1)" # 3) A record payload + create/update json_payload_a() { cat < ${current_ip}" curl -fsS -X POST "${hdr[@]}" \ --data "$(json_payload_a)" \ "${API}/zones/${zone_id}/dns_records" >/dev/null log "A record created." else log "Updating A ${CF_RECORD_NAME} -> ${current_ip}" curl -fsS -X PUT "${hdr[@]}" \ --data "$(json_payload_a)" \ "${API}/zones/${zone_id}/dns_records/${record_id}" >/dev/null log "A record updated." fi # 4) Create/Update CNAME: SHORT_ALIAS -> CF_RECORD_NAME if [[ "$SHORT_ALIAS_FQDN" == "$CF_RECORD_NAME" ]]; then log "SHORT_ALIAS equals CF_RECORD_NAME; skipping CNAME to avoid name conflict." else cname_q="type=CNAME&name=$(urlenc "$SHORT_ALIAS_FQDN")" cname_json="$(curl -fsS "${hdr[@]}" "${API}/zones/${zone_id}/dns_records?${cname_q}")" cname_id="$(echo "$cname_json" | sed -n 's/.*"id":"\([^"]*\)".*"name":"'"$SHORT_ALIAS_FQDN"'".*"type":"CNAME".*/\1/p' | head -n1)" json_payload_cname() { cat < ${CF_RECORD_NAME}" curl -fsS -X POST "${hdr[@]}" \ --data "$(json_payload_cname)" \ "${API}/zones/${zone_id}/dns_records" >/dev/null log "CNAME created." else log "Updating CNAME ${SHORT_ALIAS_FQDN} -> ${CF_RECORD_NAME}" curl -fsS -X PUT "${hdr[@]}" \ --data "$(json_payload_cname)" \ "${API}/zones/${zone_id}/dns_records/${cname_id}" >/dev/null log "CNAME updated." fi fi echo "$current_ip" > "$LAST_IP_FILE"