Skip to content

Instantly share code, notes, and snippets.

@larrybolt
Last active April 29, 2024 13:34
Show Gist options
  • Save larrybolt/6295160 to your computer and use it in GitHub Desktop.
Save larrybolt/6295160 to your computer and use it in GitHub Desktop.
Automatically update your CloudFlare DNS record to the IP, Dynamic DNS for Cloudflare
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
# Automatically update your CloudFlare DNS record to the IP, Dynamic DNS
# Can retrieve cloudflare Domain id and list zone's, because, lazy
# Place at:
# /usr/local/bin/cf-ddns.sh
# run `crontab -e` and add next line:
# 0 * * * * /usr/local/bin/cf-ddns.sh >/dev/null 2>&1
# if you're lazy (like me) copy/paste the command BETWEEN the EOT
: <<'EOT'
curl https://gist.githubusercontent.com/larrybolt/6295160/raw > /usr/local/bin/cf-ddns.sh && chmod +x /usr/local/bin/cf-ddns.sh
(crontab -l 2>/dev/null; echo "0 * * * * /usr/local/bin/cf-ddns.sh >/dev/null 2>&1") | crontab -
$EDITOR /usr/local/bin/cf-ddns.sh
/usr/local/bin/cf-ddns.sh
EOT
# run /usr/local/bin/cf-ddns.sh in terminal to check all settings are valid
# Usage:
# cf-ddns.sh -k cloudflare-api-key \
# -u user@example.com \
# -h host.example.com \ # fqdn of the record you want to update
# -z example.com \ # will show you all zones if forgot, but you need this
# Optional flags:
# -i cloudflare-record-id \ # script will show this
# -a true|false \ # auto get zone list and record id
# -f false|true \ # force dns update, disregard local stored ip
# default config
# API key, see https://www.cloudflare.com/a/account/my-account,
# incorrect api-key results in E_UNAUTH error
CFKEY=
# Zone name, will list all possible if missing, eg: example.com
CFZONE=
# Domain id, will retrieve itself by default
CFID=
# Username, eg: user@example.com
CFUSER=
# Hostname to update, eg: homeserver.example.com
CFHOST=
# Cloudflare TTL for record, between 120 and 86400 seconds
CFTTL=3600
# Get domain ID from Cloudflare using awk/sed and python json.tool
GETID=true
# Ignore local file, update ip anyway
FORCE=false
# Site to retrieve WAN ip, other examples are: bot.whatismyipaddress.com, https://api.ipify.org/ ...
WANIPSITE="http://icanhazip.com"
# get parameter
while getopts a:k:i:u:h:z:f: opts; do
case ${opts} in
a) GETID=${OPTARG} ;;
k) CFKEY=${OPTARG} ;;
i) CFID=${OPTARG} ;;
u) CFUSER=${OPTARG} ;;
h) CFHOST=${OPTARG} ;;
z) CFZONE=${OPTARG} ;;
f) FORCE=${OPTARG} ;;
esac
done
# If required settings are missing just exit
if [ "$CFKEY" = "" ]; then
echo "Missing api-key, get at: https://www.cloudflare.com/a/account/my-account"
echo "and save in ${0} or using the -k flag"
exit 2
fi
if [ "$CFUSER" = "" ]; then
echo "Missing username, probably your email-address"
echo "and save in ${0} or using the -u flag"
exit 2
fi
if [ "$CFHOST" = "" ]; then
echo "Missing hostname, what host do you want to update?"
echo "save in ${0} or using the -h flag"
exit 2
fi
# If the hostname is not a FQDN
if [ "$CFHOST" != "$CFZONE" ] && ! [ -z "${CFHOST##*$CFZONE}" ]; then
CFHOST="$CFHOST.$CFZONE"
echo " => Hostname is not a FQDN, assuming $CFHOST"
fi
# If CFZONE is missing, retrieve them all from CF
if [ "$CFZONE" = "" ]; then
echo "Missing zone"
if ! [ "$GETID" == true ]; then exit 2; fi
echo "listing all zones: (if api-key is valid)"
curl -s https://www.cloudflare.com/api_json.html \
-d a=zone_load_multi \
-d tkn=$CFKEY \
-d email=$CFUSER \
| grep -Eo '"zone_name":"([^"]+)"' \
| cut -d':' -f2 \
| awk '{gsub("\"","");print "* "$1}'
echo "Please specify the matching zone in ${0} or specify using the -z flag"
exit 2
fi
# Get current and old WAN ip
WAN_IP=`curl -s ${WANIPSITE}`
if [ -f $HOME/.wan_ip-cf.txt ]; then
OLD_WAN_IP=`cat $HOME/.wan_ip-cf.txt`
else
echo "No file, need IP"
OLD_WAN_IP=""
fi
# If WAN IP is unchanged an not -f flag, exit here
if [ "$WAN_IP" = "$OLD_WAN_IP" ] && [ "$FORCE" = false ]; then
echo "WAN IP Unchanged, to update anyway use flag -f true"
exit 0
fi
# If CFID is missing retrieve and use it
if [ "$CFID" = "" ]; then
echo "Missing DNS record ID"
if ! [ "$GETID" == true ]; then exit 2; fi
echo "fetching from Cloudflare..."
if ! CFID=$(
curl -s https://www.cloudflare.com/api_json.html \
-d a=rec_load_all \
-d tkn=$CFKEY \
-d email=$CFUSER \
-d z=$CFZONE \
| grep -Eo '"(rec_id|name|type)":"([^"]+)"' \
| cut -d':' -f2 \
| awk 'NR%3{gsub("\"","");printf $0" ";next;}1' \
| grep -E "${CFHOST//./\\.}" \
| grep -e '"A"' \
| grep -Eo "(^|\s)(\d+)(\s|$)"
); then
echo " => Incorrect zone, or zone doesn't contain the A-record ${CFHOST}!"
echo "listing all records for zone ${CFZONE}:"
(printf "ID RECORD TYPE\n";
curl -s https://www.cloudflare.com/api_json.html \
-d a=rec_load_all \
-d tkn=$CFKEY \
-d email=$CFUSER \
-d z=$CFZONE \
| grep -Eo '"(rec_id|name|type)":"([^"]+)"' \
| cut -d':' -f2 \
| awk 'NR%3{gsub("\"","");printf $0" ";next;}1'
)| column -t
exit 2
fi
echo " => Found CFID=${CFID}, advising to save this to ${0} or set it using the -i flag"
fi
# If WAN is changed, update cloudflare
echo "Updating DNS to $WAN_IP"
RESPONSE=$(
curl -s https://www.cloudflare.com/api_json.html \
-d a=rec_edit \
-d tkn=$CFKEY \
-d email=$CFUSER \
-d z=$CFZONE \
-d id=$CFID \
-d ttl=$CFTTL \
-d type=A \
-d name=$CFHOST \
-d "content=$WAN_IP"
)
if [ "$RESPONSE" != "${RESPONSE%success*}" ]; then
echo "Updated succesfuly!"
echo $WAN_IP > $HOME/.wan_ip-cf.txt
exit
else
echo 'Something went wrong :('
echo "Response: $RESPONSE"
exit 1
fi
@MachineITSvcs
Copy link

MachineITSvcs commented Jan 3, 2018

I recently made a script that you all may be of interest to you all. Here is a link: Github link

@DarkMatterMatt
Copy link

Thanks, nice script 😁

I had an error => Incorrect zone, or zone doesn't contain the A-record myhost.mydomain.com!, even though it was on list.
@vhugo has the fix above but I'll post it here again:
On line 144 replace grep -Eo "(^|\s)(\d+)(\s|$) with grep -Eo "(^|\s)([0-9]+)(\s|$) because grep -E doesn't have the \d escape. Alternatively you can use grep -Po "(^|\s)([0-9]+)(\s|$)"` which uses posix regex (which most of us are more familiar with).
I am using grep (GNU grep) 2.27.

Another problem I came across was multiple matches when trying to update the root domain. For example, trying to update example.com would update a.example.com because it contains the word example.com. The fix for this is:
On line 142 replace grep -E "${CFHOST//./\\.}" with grep -E "\s${CFHOST//./\\.}\s". The \s means that the grep expression will only match the whole word as opposed to parts of a word.

Forked to https://gist.github.com/DarkMatterMatt/b366e7706693edc23b8aed3cd50525a9

@jonegerton
Copy link

A very long time ago I linked my versions of these scripts.

I've just got around to updating these scripts to the latest Cloudflare APIs, and pushed them to here. They're still aimed mainly at running via crontab on a raspberry pi, so tested on Raspbian Stretch.

@DesertCookie
Copy link

DesertCookie commented Jan 8, 2020

@m3nu

https://gist.github.com/manuelRiel/62ffc2d402166f3e0694

This link is broken. Would you care to update it?

@m3nu
Copy link

m3nu commented Jan 8, 2020

Sure. Just changed my username since then. New link is:

https://gist.github.com/m3nu/62ffc2d402166f3e0694

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment