Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
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

heypete commented Aug 22, 2014

Consider replacing curl -s http://icanhazip.com with dig +short myip.opendns.com @resolver1.opendns.com. It works exactly the same.

Querying icanhazip.com or other similar sites requires a fairly expensive TCP connection, HTTP overhead, etc. This can be burdensome when queried regularly via cron jobs. A UDP-based DNS connection is considerably faster, lighter weight, and uses far less resources.

hebbet commented Sep 10, 2014

opendns.com doesn't support IPv6.

jb510 commented Dec 13, 2014

I don't know for how long it has, but FWIW OpenDNS has started supporting IPV6 in a sandbox. https://www.opendns.com/about/innovations/ipv6/

Testing:
dig +short icanhazip.com AAAA @resolver1.opendns.com
Returns IPV6 addresses

and
dig +short icanhazip.com ANY @resolver1.opendns.com
returns a mix

(I haven't tired implementing this yet, it's strictly FYI)

Mikaela commented Jan 18, 2015

How would updating AAAA record work with this script?

Stopped working for me recently, didn't make any chances. Working fine for others still? I got to wonder if they finally removed diup from the api.

Stop working for me, returned this error when I take off the silencing option

"E_INVLDINPUT
You must include an `a' parameter, with a value of wl|chl|nul|ban|comm_news|devmode|sec_lvl|ipv46|ob|cache_lvl|fpurge_ts|async|mirage2|img.q|minify|stats|zone_check|zone_ips|zone_ss|vote_ss|app|ip_lkup|set|app_req|app_req_list|app_version|custom_cert_set|custom_cert_purge|custom_cert_load_multi|custom_cert_load|ersubmit|zoneupload|user_notification_remove|pref_set|zone_file_purge|zone_file_refresh|zone_settings
Additionally, email and tkn parameters are required.
_________________________________"

:/

I have found the solution for the error, cloudflare updated their API this year and you need to rewrite the part of the script that POST's to cloudflare

http://kevo.io/code/2012/11/07/cloudflare-dynamic-dns/

remoe commented Jan 25, 2015

Here is an update:

        curl https://www.cloudflare.com/api_json.html \
          -d a=rec_edit \
          -d tkn=$cfkey \
          -d email=$cfuser \
          -d z=$cfhost \
          -d id=$cfid \
          -d type=A \
          -d name=$cfhost \
          -d ttl=1 \
          -d "content=$WAN_IP"

edit: ttl=1 doesn't work

abyssdj commented Feb 12, 2015

Forgive my coding ignorance, but will this script update multiple domains at once or would I need to run a separate script for each domain? For example *.blah.com, real.blah.com, *.otherblah.com and whatever.otherblah.com?

Also, could somebody tell me where I can find the cfid? I don't remember using it in the last script I had, just the key.

To get the cfid (aka rec_id), the below script will get the dns records from cloudflare. You can find the rec_id to use from the downloaded json:

#!/bin/sh
# Reads all dns records for a cloudflare hosted domain.
# Use this to get the domain id required for cloudflare ddns updates 
# (is rec_id in the saved json at ~/.cf-dns.txt)
#
# Created 25-Feb-2015 Jon Egerton, from the rec_load_all documentation here: 
# https://www.cloudflare.com/docs/client-api.html
#
# This version is working as at 25-Feb-2015
# As/When cloudflare change their API amendments may be required

cfkey=API_KEY_HERE
cfuser=EMAIL_HERE
domain=DOMAIN_HERE

curl https://www.cloudflare.com/api_json.html \
  -d a=rec_load_all \
  -d tkn=$cfkey \
  -d email=$cfuser \
  -d z=$domain > $HOME/.cf-dns.txt

I'm maintaining my versions of these scripts here.

dodysw commented Jul 26, 2015

(not sure why I can't edit nor delete my previous comment, ignore the previous one)
The above end point is old api (v1) which works and is still supported. However Cloudflare recommends new clients to use the new API (v4). Here is an example based on the above bash script:

#!/bin/sh
cfkey=API_KEY_HERE
cfuser=EMAIL_HERE
cfhost=HOST_NAME_TO_CHANGE
zoneid=ZONE_ID
dnsrecid=DNS_RECORD_ID

curl -X PUT "https://api.cloudflare.com/client/v4/zones/$zoneid/dns_records/$dnsrecid" \
-H "X-Auth-Email: $cfuser" \
-H "X-Auth-Key: $cfkey" \
-H "Content-Type: application/json" \
--data '{"id":"'$dnsrecid'","type":"A","name":"'$cfhost'","content":"'`curl -s http://icanhazip.com`'"}'

The tricky part on the new API is getting the zone id and dns record id as afaik it is not exposed on the control panel. There are two way to do this:

1. Via api

We can utilize the API itself to give us the answers:

# Get list of zone ids
curl -s -X GET "https://api.cloudflare.com/client/v4/zones" \
-H "X-Auth-Email: $cfuser" \
-H "X-Auth-Key: $cfkey" \
-H "Content-Type: application/json" \
| jq '.result[] | {name, id}'

# put the id returned above into here
zoneid=ZONE_ID

# Get list of DNS record ids in a zone
curl -s GET "https://api.cloudflare.com/client/v4/zones/$zoneid/dns_records" \
-H "X-Auth-Email: $cfuser" \
-H "X-Auth-Key: $cfkey" \
-H "Content-Type: application/json" \
| jq '.result[] | {name, id, zone_id, zone_name, content, type}'

Take note of the zone id and the id corresponding to the dns record to update and use them on the first script. If you don't have jq installed, remove that line.

2. Via Chrome browser

Open the Chrome developer tools, open the network tab, click XHR. Navigate to Cloudflare DNS setting for the domain name. The zone id and the dns record id will be visible on the preview tab of dns_records?page=.. entry in the network tab.

This gist was very helpful! I have written a client in golang that will take an email, apikey, and list of hostnames and will update any records that are 'A' records and match one of the hostnames given. It will not update the record if the record is already correct. There is also a systemd service that you can install, along with a timer that will ensure your records are updated as often as you like.

Hope it's of use to someone! https://github.com/colemickens/cloudflare-dyndns

  1. IPv4 and IPv6 support (A, AAAA)
  2. Support for multiple ddns records
  3. OS independent Python script with no non-standard dependencies
  4. Automatically extracts zone and record IDs so you don't have to
  5. Only updates records if the IP has changes
  6. https://github.com/asazodi/cloudflare_ddns
Owner

larrybolt commented Sep 19, 2015

Updated this script to work again, it's not a cloudflare cli management tool, it just does the job

  • it's a bash script!
  • lists the zones for easy updating
  • auto-retrieves the record id
  • works with both configs you set inside the script, or/and flags
  • only needs curl to update dns-record
  • for auto-finding zones and record-id only requires awk,sed and python>2.6

I'm working on a modified version of this script but i keep getting this error when trying to update:

"Editing types after a record has been created is not allowed"

Has anyone come across this error before - im stuck.

Fantastic - Had an issue where a firewall was blocking anything that was coming in via Dynamic DNS - I was using a CNAME to a no-ip.org address to get to my home server. Whacking this on the Raspberry Pi sorted the issue in under 10 minutes :) I'll keep the no-ip agent running to ensure some level of failover, fantastic script :)

I have updated it to use the dig +short myip.opendns.com @resolver1.opendns.com command instead and so far perfect.

Owner

larrybolt commented Sep 29, 2015

@snipermd sounds like the record you created wasn't an A-record, if you are still having problems mail me (see github profile) and I'll help you as good as I can!

@adammatthews Awesome! 😄 Glad to know it works out of the box on RPi! Right? Otherwise that awk/sed-voodoo was for nothing :(

Owner

larrybolt commented Nov 8, 2015

Made it easier and clearer, removed python so should work in a mostly POSIX-compliant OS (I think)

m3nu commented Jan 19, 2016

I'm too lazy to actually find out zone and record ID. So I made the same thing in Python. Only real parameter is the subdomain. E.g. you can update server host1.tld.com by calling

cloudflare-update.py host1

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

Only dependency is the python-requests package that should be available in all distributions.

Or just use a CNAME to an NO-IP 💃

vhugo commented Mar 2, 2016

Hey man, really cool script, thanks for making it!!

I had a small issue, the script wasn't finding my host and saying => Incorrect zone, or zone doesn't contain the A-record myhost.mydomain.com!, even though it was on list, so I checked out the code and line 144 was broken for me, to fix it I had to replace (^|\s)(\d+)(\s|$) for (^|\s)([0-9]+)(\s|$), I know it's the same thing, but now it's working perfectly.

FYI - using grep (GNU grep) 2.23

Thanks @vhugo, just had the same problem and applying your fix made it work.

gfjardim commented Mar 2, 2016

Thanks @vhugo, that worked for me too. Why not change the piped and nested code to something clearer using arrays?

# 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)"
  JSON=$(curl -s https://www.cloudflare.com/api_json.html \
       -d a=zone_load_multi \
       -d tkn=$CFKEY \
       -d email=$CFUSER | sed -e 's/":"/:/g')

  if [ -n "$(echo $JSON|grep -Po '"err_code:\K[^"]*')" ]; then
    echo "Error:" $(echo -e $JSON|grep -Po '"msg:\K[^"]*')
    exit 2
  fi

  for m in $(echo $JSON|grep -Po '"zone_name:\K[^"]*'); do
    echo "* $m"
  done

  echo "Please specify the matching zone in ${0} or specify using the -z flag"
  exit 2
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..."

  JSON=$(curl -s https://www.cloudflare.com/api_json.html \
       -d a=rec_load_all \
       -d tkn=$CFKEY \
       -d email=$CFUSER \
       -d z=$CFZONE | sed -e 's/":"/:/g')

  if [ -n "$(echo $JSON|grep -Po '"err_code:\K[^"]*')" ]; then
    echo "Error:" $(echo -e $JSON|grep -Po '"msg:\K[^"]*')
    exit 2
  fi

  IDS=($(echo $JSON|grep -Po '"rec_id:\K[^"]*'))
  NAMES=($(echo $JSON|grep -Po '"name:\K[^"]*'))
  TYPES=($(echo $JSON|grep -Po '"type:\K[^"]*'))
  RECORDS="ID RECORD TYPE";

  for key in $(seq 0 $((${#IDS[@]}-1)) ); do
    if [ "${NAMES[$key]}" == "$CFHOST" ] && [ "${TYPES[$key]}" == "A" ]; then
      CFID=${IDS[$key]}
    fi
    RECORDS="${RECORDS}\n${IDS[$key]} ${NAMES[$key]} ${TYPES[$key]}"
  done

  if [ -z "$CFID" ]; then
    echo " => Incorrect zone, or zone doesn't contain the A-record ${CFHOST}!"
    echo "listing all records for zone ${CFZONE}:"
    echo -e "$RECORDS"|column -t
    exit 2
  fi
@ghost

ghost commented Mar 25, 2016

Thanks @vhugo and @heypete, current script need to apply patch from those two for working!

bthome commented Apr 8, 2016

@gfjardim implemented your changes, works great. You are missing an "fi" at the end.
@vhugo and @heypete made the updates as well.

@larrybolt thanks for the script. Simple and effective.

gstuartj commented May 6, 2016

@toxiicdev Using a CNAME to a no-ip address has drawbacks including revealing your real IP address and some lookup performance considerations.

line 72: getopts: not found

I have a dual WAN connection. One of the connections is a static IP address but the faster consumer line is a dynamic ip address. This script helped me solve the problem of tracking it. :)

Khampol commented Jul 22, 2016

Hello,
I update my (dynamic) ip cloudflare by pfsense. I normally use API v1. Now cloudflare will close it and i have to use API v4.

For v1 i use this url =
https://www.cloudflare.com/api_json.html?a=rec_edit&tkn=d82f21f45473a631584b06b4xxxxxxxxxxxxxxxxxx&id=xxxxxx9306&email=xxxxxxxxx@gmail.com&z=xxxxxxxxx.info&type=A&name=xxxxxxxxxx.info&content=%IP%&service_mode=0&ttl=1

For v4 it suppose to be =
https://api.cloudflare.com/client/v4...... .....

Need help...

WARNING Noob here. :)

Can this be into a DD-WRT config via their Command Shell or other means? If so, which of the above methods is best suited for DD-WRT methods?

If not, any ideas on how to effect the change in CF via a DD-WRT router?

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