Cloudflare Dynamic DNS

143 lines

Small bash script to dynamically update Cloudflare DNS records to the current external IP addresses.

  • Supports both IPv4 and IPv6: A and AAAA
  • Multi-source comparison (prevents against source poisoning/failure)
  • Cloudflare API v4
  • Configurable
#!/bin/bash

# https://reuben.science

red='\033[1;31m'
yellow='\033[1;33m'
green='\033[1;32m'
purple='\033[1;35m'
reset='\033[0m'

# Cloudflare Zone ID - copy from Cloudflare DNS page
ZONE_ID=979f5e5c765123d539fad5e744d3fd10

# Full domain to be updated
DNS_RECORD=reuben.science

# Cloudflare Token - scoped for write access to the desired Zone.Zone, Zone.DNS
CLOUDFLARE_TOKEN=6ktwpvFqyn5U0m0SBEyLt2nPQzSTLy3K_TLZVfXi

# How many IP addresses must agree, in order for the result to be used?
IPv4_THRESHOLD=3
IPv6_THRESHOLD=3

# Get the current external IP addresses
IPv4=(
  $(curl -s -m 4 https://api.ipify.org)
  $(curl -s -m 4 https://ipv4.wtfismyip.com/text)
  $(curl -s -m 4 https://api4.my-ip.io/ip)
  $(curl -s -m 4 https://v4.ipv6-test.com/api/myip.php)
  $(curl -s -m 4 https://ipv4.lookup.test-ipv6.com/ | jq -r '.ip')
)

IPv6=(
  $(curl -s -m 4 https://api6.ipify.org)
  $(curl -s -m 4 https://ipv6.wtfismyip.com/text)
  $(curl -s -m 4 https://api6.my-ip.io/ip)
  $(curl -s -m 4 https://v6.ipv6-test.com/api/myip.php)
  $(curl -s -m 4 https://ipv6.lookup.test-ipv6.com/ | jq -r '.ip')
)

# Pass Results to awk
IPv4_AWK=($(printf '%s\n' "${IPv4[@]}" | awk ' \
  BEGIN {va = 0; max = 0} \
  max < ++c[$0] {if(length($0) != 0){max = c[$0]; addr = $0}} \
  {if(length($0) != 0){va = ++va}} \
  END {print (length(addr) != 0) ? addr : "NULL", va, max, NR} \
'))

IPv6_AWK=($(printf '%s\n' "${IPv6[@]}" | awk ' \
  BEGIN {va = 0; max = 0} \
  max < ++c[$0] {if(length($0) != 0){max = c[$0]; addr = $0}} \
  {if(length($0) != 0){va = ++va}} \
  END {print (length(addr) != 0) ? addr : "NULL", va, max, NR} \
'))

# Get the most common value for each service, ignoring empty responses
IPv4_ADDR=${IPv4_AWK[0]/NULL/}
IPv6_ADDR=${IPv6_AWK[0]/NULL/}

# How many services returned addresses?
IPv4_RETURNED=${IPv4_AWK[1]}
IPv6_RETURNED=${IPv6_AWK[1]}

# How many services agreed?
IPv4_AGREED=${IPv4_AWK[2]}
IPv6_AGREED=${IPv6_AWK[2]}

# How many services in total?
IPv4_TOTAL=${IPv4_AWK[3]}
IPv6_TOTAL=${IPv6_AWK[3]}

# Successful?
IPv4_SUCCESS=""
IPv6_SUCCESS=""

if [ -z "$IPv4_ADDR" ]
then
  echo -e "${red}No IPv4 Address Found${reset}" >&2
elif [ "$((IPv4_AGREED))" -ge "$((IPv4_THRESHOLD))" ] # Protect against poisoned data
then
	DNS_RECORDIDv4=$(curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=A&name=$DNS_RECORD" \
  -H "Authorization: Bearer $CLOUDFLARE_TOKEN" \
  -H "Content-Type: application/json" | jq -r '{"result"}[] | .[0] | .id')

  response=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$DNS_RECORDIDv4" \
    -H "Authorization: Bearer $CLOUDFLARE_TOKEN" \
    -H "Content-Type: application/json" \
    --data "{\"type\":\"A\",\"name\":\"$DNS_RECORD\",\"content\":\"$IPv4_ADDR\",\"ttl\":1,\"proxied\":false}")

  if [ "$(echo "${response}" | jq -r '.success')" = "true" ]
  then
    echo -e "A    Record Updated -> ${yellow}$IPv4_ADDR${reset} ${green}${IPv4_AGREED}${reset}/${IPv4_RETURNED}/${IPv4_TOTAL}"
    IPv4_SUCCESS="YES"
  else
    echo -e "A    Record Failed: ${red}${response}${reset}" >&2
  fi

else
	echo -e "Not Enough IPv4 Addresses Agreed: ${red}${IPv4_AGREED}${reset}/${IPv4_RETURNED}/${IPv4_TOTAL}" >&2
fi

if [ -z "$IPv6_ADDR" ]
then
  echo -e "${red}No IPv6 Address Found${reset}" >&2
elif [ "$((IPv6_AGREED))" -ge "$((IPv6_THRESHOLD))" ] # Protect against poisoned data
then
	DNS_RECORDIDv6=$(curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=AAAA&name=$DNS_RECORD" \
  -H "Authorization: Bearer $CLOUDFLARE_TOKEN" \
  -H "Content-Type: application/json" | jq -r '{"result"}[] | .[0] | .id')

  response=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$DNS_RECORDIDv6" \
    -H "Authorization: Bearer $CLOUDFLARE_TOKEN" \
    -H "Content-Type: application/json" \
    --data "{\"type\":\"AAAA\",\"name\":\"$DNS_RECORD\",\"content\":\"$IPv6_ADDR\",\"ttl\":1,\"proxied\":false}")

    if [ "$(echo "${response}" | jq -r '.success')" = "true" ]
    then
    	echo -e "AAAA Record Updated -> ${purple}$IPv6_ADDR${reset} ${green}${IPv6_AGREED}${reset}/${IPv6_RETURNED}/${IPv6_TOTAL}"
      IPv6_SUCCESS="YES"
    else
      echo -e "AAAA Record Failed: ${red}${response}${reset}" >&2
    fi
else
	echo -e "Not Enough IPv6 Addresses Agreed: ${red}${IPv6_AGREED}${reset}/${IPv6_RETURNED}/${IPv6_TOTAL}" >&2
fi

if [ -n "$IPv4_ADDR" ]
then
  if [ -z "$IPv4_SUCCESS" ]
  then
    exit 1
  fi
fi

if [ -n "$IPv6_ADDR" ]
then
  if [ -z "$IPv6_SUCCESS" ]
  then
    exit 1
  fi
fi

exit 0