Skip to content

Instantly share code, notes, and snippets.

@lifehome
Forked from benkulbertis/cloudflare-update-record.sh
Last active February 25, 2024 06:34
Show Gist options
  • Star 50 You must be signed in to star a gist
  • Fork 12 You must be signed in to fork a gist
  • Save lifehome/eb3f7d798f9bc6720cdc7d2be1238d4f to your computer and use it in GitHub Desktop.
Save lifehome/eb3f7d798f9bc6720cdc7d2be1238d4f to your computer and use it in GitHub Desktop.
Cloudflare API v4 Dynamic DNS Update in Bash

Cloudflare DDNS bash client with systemd

This is a bash script to act as a Cloudflare DDNS client, useful replacement for ddclient.

Look out!

A newer version is available!

This gist will no longer update, instead please go to https://github.com/lifehome/systemd-cfddns for more updated versions.

How to use?

  1. Put the cfupdater files to /usr/local/bin
  • If you are using IPv4 for A record, append -v4 to cfupdater in the following systemd service unit.
  • If you are using IPv6 for AAAA record, append -v6 to cfupdater in the following systemd service unit.
  • If you prefer a dual-stack record, append -dualstack to cfupdater in the following systemd service unit.
  1. chmod +x /usr/local/bin/cfupdater
  2. Create a systemd service unit at /etc/systemd/system/, the cfupdate.service is shown as an example.
  3. Create a systemd timer unit at the same location of the service unit, the cfupdate.timer is shown as an example.
  4. sudo systemctl enable cfupdate.timer
  5. sudo systemctl start cfupdate.timer

Note

The default cfupdate.timer is set to execute the script every minute.
Please keep in mind not to spam the API or you will be rate limited.

The dual-stack script has NOT been tested, use with caution. The dual-stack script will always sync upon either IPv4 or IPv6 has changed.

A quote from Cloudflare FAQ:

All calls through the Cloudflare Client API are rate-limited to 1200 every 5 minutes.

-- https://support.cloudflare.com/hc/en-us/articles/200171456

[Unit]
Description=Cloudflare DDNS service
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/cfupdater
[Unit]
Description=Run cfupdate.service every minute
[Timer]
OnCalendar=*:0/1
Unit=cfupdate.service
[Install]
WantedBy=multi-user.target
#!/bin/bash
# Forked by benkulbertis/cloudflare-update-record.sh
# CHANGE THESE
auth_email="john.appleseed@example.org" # The email used to login 'https://dash.cloudflare.com'
auth_key="f1nd7h47fuck1n6k3y1ncl0udfl4r3c0n50l3" # Top right corner, "My profile" > "Global API Key"
zone_identifier="f1nd7h3fuck1n6z0n31d3n71f13r4l50" # Can be found in the "Overview" tab of your domain
record_name="ipv4.example.org" # Which record you want to be synced
# DO NOT CHANGE LINES BELOW
ip4=$(curl -s https://ipv4.icanhazip.com/)
ip6=$(curl -s https://ipv6.icanhazip.com/)
# SCRIPT START
echo "[Cloudflare DDNS] Check Initiated"
# Seek for the record
record4=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$record_name&type=A" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json")
record6=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$record_name&type=AAAA" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json")
# Can't do anything without both record
if [[ $record4 == *"\"count\":0"* || $record6 == *"\"count\":0"* ]]; then
>&2 echo -e "[Cloudflare DDNS] Dual stack records do not exist, perhaps create them first?"
exit 1
fi
# Set existing IP address from the fetched record
old_ip4=$(echo "$record4" | grep -Po '(?<="content":")[^"]*' | head -1)
old_ip6=$(echo "$record6" | grep -Po '(?<="content":")[^"]*' | head -1)
# Compare either one is the same
# NOTE: The script will update even one IP remains the same.
if [[ $ip4 == $old_ip4 && $ip6 == $old_ip6 ]]; then
echo "[Cloudflare DDNS] IPs have not changed."
exit 0
fi
# Set the record identifier from result
record4_identifier=$(echo "$record4" | grep -Po '(?<="id":")[^"]*' | head -1)
record6_identifier=$(echo "$record6" | grep -Po '(?<="id":")[^"]*' | head -1)
# The execution of update
update4=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record4_identifier" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" --data "{\"id\":\"$zone_identifier\",\"type\":\"A\",\"proxied\":false,\"name\":\"$record_name\",\"content\":\"$ip4\"}")
update6=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record6_identifier" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" --data "{\"id\":\"$zone_identifier\",\"type\":\"AAAA\",\"proxied\":false,\"name\":\"$record_name\",\"content\":\"$ip6\"}")
# The moment of truth
if [[ $update4 == *"\"success\":false"* || $update6 == *"\"success\":false"* ]]; then
>&2 echo -e "[Cloudflare DDNS] Update failed. DUMPING RESULTS:\n$update4\n$update6"
exit 1
else
echo "[Cloudflare DDNS] IPv4 address '$ip4' and IPv6 address '$ip6' has been synced to Cloudflare."
fi
#!/bin/bash
# Forked by benkulbertis/cloudflare-update-record.sh
# CHANGE THESE
auth_email="john.appleseed@example.org" # The email used to login 'https://dash.cloudflare.com'
auth_key="f1nd7h47fuck1n6k3y1ncl0udfl4r3c0n50l3" # Top right corner, "My profile" > "Global API Key"
zone_identifier="f1nd7h3fuck1n6z0n31d3n71f13r4l50" # Can be found in the "Overview" tab of your domain
record_name="ipv4.example.org" # Which record you want to be synced
# DO NOT CHANGE LINES BELOW
ip=$(curl -s https://ipv4.icanhazip.com/)
# SCRIPT START
echo "[Cloudflare DDNS] Check Initiated"
# Seek for the record
record=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$record_name" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json")
# Can't do anything without the record
if [[ $record == *"\"count\":0"* ]]; then
>&2 echo -e "[Cloudflare DDNS] Record does not exist, perhaps create one first?"
exit 1
fi
# Set existing IP address from the fetched record
old_ip=$(echo "$record" | grep -Po '(?<="content":")[^"]*' | head -1)
# Compare if they're the same
if [ $ip == $old_ip ]; then
echo "[Cloudflare DDNS] IP has not changed."
exit 0
fi
# Set the record identifier from result
record_identifier=$(echo "$record" | grep -Po '(?<="id":")[^"]*' | head -1)
# The execution of update
update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record_identifier" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" --data "{\"id\":\"$zone_identifier\",\"type\":\"A\",\"proxied\":false,\"name\":\"$record_name\",\"content\":\"$ip\"}")
# The moment of truth
case "$update" in
*"\"success\":false"*)
>&2 echo -e "[Cloudflare DDNS] Update failed for $record_identifier. DUMPING RESULTS:\n$update"
exit 1;;
*)
echo "[Cloudflare DDNS] IPv4 context '$ip' has been synced to Cloudflare.";;
esac
#!/bin/bash
# Forked by benkulbertis/cloudflare-update-record.sh
# CHANGE THESE
auth_email="john.appleseed@example.org" # The email used to login 'https://dash.cloudflare.com'
auth_key="f1nd7h47fuck1n6k3y1ncl0udfl4r3c0n50l3" # Top right corner, "My profile" > "Global API Key"
zone_identifier="f1nd7h3fuck1n6z0n31d3n71f13r4l50" # Can be found in the "Overview" tab of your domain
record_name="ipv6.example.org" # Which record you want to be synced
# DO NOT CHANGE LINES BELOW
ip=$(curl -s https://ipv6.icanhazip.com/)
# SCRIPT START
echo "[Cloudflare DDNS] Check Initiated"
# Seek for the record
record=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$record_name" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json")
# Can't do anything without the record
if [[ $record == *"\"count\":0"* ]]; then
>&2 echo -e "[Cloudflare DDNS] Record does not exist, perhaps create one first?"
exit 1
fi
# Set existing IP address from the fetched record
old_ip=$(echo "$record" | grep -Po '(?<="content":")[^"]*' | head -1)
# Compare if they're the same
if [ $ip == $old_ip ]; then
echo "[Cloudflare DDNS] IP has not changed." | systemd-cat -p notice
exit 0
fi
# Set the record identifier from result
record_identifier=$(echo "$record" | grep -Po '(?<="id":")[^"]*' | head -1)
# The execution of update
update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record_identifier" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" --data "{\"id\":\"$zone_identifier\",\"type\":\"AAAA\",\"proxied\":false,\"name\":\"$record_name\",\"content\":\"$ip\"}")
# The moment of truth
case "$update" in
*"\"success\":false"*)
>&2 echo -e "[Cloudflare DDNS] Update failed for $record_identifier. DUMPING RESULTS:\n$update"
exit 1;;
*)
echo "[Cloudflare DDNS] IPv6 address '$ip' has been synced to Cloudflare.";;
esac
@Gondost
Copy link

Gondost commented Mar 16, 2020

Here's an update that allows you to specify multiple domains, sub domains, zones, and accounts, all from one script.

#!/bin/bash

# CHANGE THESE
declare -A EMAILKEY=(
        # Login email and Global API key
        # [auth_email]=auth_key
        [cfloginemail@domain.com]=yourcloudflareapikey
)

declare -A RECORDEMAIL=(
        # [domain]=auth_email
        [sub.domain.com]=cfloginemail@domain.com
        [domain.com]=cfloginemail@domain.com
)

declare -A RECORDZONE=(
        # [record_name]=zone_identifier
        [sub.domain.com]=yourcloudflaredomainzoneid
        [domain.com]=yourcloudflaredomainzoneid
)

# DO NOT CHANGE LINES BELOW
ip=$(curl -s https://ipv4.icanhazip.com/)

# SCRIPT START

for record_name in "${!RECORDZONE[@]}"
do
  # Get all the required values from associative arrays
  zone_identifier=${RECORDZONE[$record_name]}
  auth_email=${RECORDEMAIL[$record_name]}
  auth_key=${EMAILKEY[$auth_email]}
  echo "[Cloudflare DDNS] Check Initiated for $record_name"

  # Seek for the record
  record=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$record_name" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json")

  # Can't do anything without the record
  if [[ $record == *"\"count\":0"* ]]; then
    >&2 echo -e "[Cloudflare DDNS] Record does not exist, perhaps create one first?"
    continue
  fi

  # Set existing IP address from the fetched record
  old_ip=$(echo "$record" | grep -Po '(?<="content":")[^"]*' | head -1)

  # Compare if they're the same
  if [ $ip == $old_ip ]; then
    echo "[Cloudflare DDNS] IP for $record_name has not changed."
    continue
  fi

  # Set the record identifier from result
  echo "[Cloudflare DDNS] Old IP was $old_ip, trying to set new IP $ip"
  record_identifier=$(echo "$record" | grep -Po '(?<="id":")[^"]*' | head -1)

  # The execution of update
  update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record_identifier" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" --data "{\"id\":\"$zone_identifier\",\"type\":\"A\",\"proxied\":false,\"name\":\"$record_name\",\"content\":\"$ip\"}")

  # The moment of truth
  case "$update" in
  *"\"success\":false"*)
    >&2 echo -e "[Cloudflare DDNS] Update failed for $record_identifier. IP is still $old_ip. DUMPING RESULTS:\n$update"
    continue;;
  *)
    echo "[Cloudflare DDNS] IPv4 context '$ip' for $record_name has been synced to Cloudflare.";;
  esac

done```

@kyreal
Copy link

kyreal commented May 18, 2020

Anyone having issues since today with the update script?
Seems i get a good answer from CF but the ip's are not updated.

I've been having issues with it recently. Running it on a mac and it seems like the grep -Po commands aren't returning anything any more when they used to.

I've got a different version of grep from homebrew to see if that resolves it and although other grep -Po commands are working these specific ones aren't returning anything. I don't know enough about regex or grep to get the right stuff out.

@lifehome
Copy link
Author

I've been having issues with it recently. Running it on a mac and it seems like the grep -Po commands aren't returning anything any more when they used to.

I've got a different version of grep from homebrew to see if that resolves it and although other grep -Po commands are working these specific ones aren't returning anything. I don't know enough about regex or grep to get the right stuff out.

Hi @kyreal, sorry to hear your experience. It is unfortunate that macOS is using a different version of grep, and thus this script is no way to be executed correctly on macOS. I am currently brewing in a new repository, but yet to have an ETA.

@dinamic
Copy link

dinamic commented Mar 4, 2021

@kyreal you could refactor out the usage of grep - I've used jq in my custom script that's heavily inspired by this one.

https://gist.github.com/dinamic/335bb0ed0e73e1b6af3b3f59c9fe08d1

@AjmalPraveeN
Copy link

Updated the v4 script to accept an array of domains or subdomains and loop through them, updating each as needed. I updated some of the script echoes as well to be more descriptive with the new loop.

#!/bin/bash

# CHANGE THESE
auth_email="john.appleseed@example.org"            # The email used to login 'https://dash.cloudflare.com'
auth_key="f1nd7h476k3y1ncl0udfl4r3c0n50l3"   # Top right corner, "My profile" > "Global API Key"
zone_identifier="f1nd7h3z0n31d3n71f13r4l50" # Can be found in the "Overview" tab of your domain
record_name_array=( "ip1v4.example.org" "ip2v4.example.org" )  # Which record(s) you want to be synced

# DO NOT CHANGE LINES BELOW
ip=$(curl -s https://ipv4.icanhazip.com/)

# SCRIPT START

for record_name in "${record_name_array[@]}"
do

  echo "[Cloudflare DDNS] Check Initiated for $record_name"

  # Seek for the record
  record=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$record_name" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json")

  # Can't do anything without the record
  if [[ $record == *"\"count\":0"* ]]; then
    >&2 echo -e "[Cloudflare DDNS] Record does not exist, perhaps create one first?"
    exit 1
  fi

  # Set existing IP address from the fetched record
  old_ip=$(echo "$record" | grep -Po '(?<="content":")[^"]*' | head -1)

  # Compare if they're the same
  if [ $ip == $old_ip ]; then
    echo "[Cloudflare DDNS] IP for $record_name has not changed."
    exit 0
  fi

  # Set the record identifier from result
  echo "[Cloudflare DDNS] Old IP was $old_ip, trying to set new IP $ip"
  record_identifier=$(echo "$record" | grep -Po '(?<="id":")[^"]*' | head -1)

  # The execution of update
  update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record_identifier" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" --data "{\"id\":\"$zone_identifier\",\"type\":\"A\",\"proxied\":false,\"name\":\"$record_name\",\"content\":\"$ip\"}")

  # The moment of truth
  case "$update" in
  *"\"success\":false"*)
    >&2 echo -e "[Cloudflare DDNS] Update failed for $record_identifier. IP is still $old_ip. DUMPING RESULTS:\n$update"
    exit 1;;
  *)
    echo "[Cloudflare DDNS] IPv4 context '$ip' for $record_name has been synced to Cloudflare.";;
  esac

done

❤ Thank you..

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