Skip to content

Instantly share code, notes, and snippets.

@badrianiulian
Forked from lifehome/README.md
Last active November 16, 2023 08:28
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save badrianiulian/7abf81d4c814d699091b8beff439ad34 to your computer and use it in GitHub Desktop.
Save badrianiulian/7abf81d4c814d699091b8beff439ad34 to your computer and use it in GitHub Desktop.
Cloudflare API v4 Dynamic DNS Update in Bash for IPv4/IPv6 records

Cloudflare DDNS bash client with systemd

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

How to use?

  1. Put the cfupdater file to /usr/local/bin
  2. chmod +x /usr/local/bin/cfupdater
  3. Create a systemd service unit at /etc/systemd/system/, the cfupdate.service is shown as an example.
  4. Create a systemd timer unit at the same location of the service unit, the cfupdate.timer is shown as an example.

Note

The default cfupdate.timer is set to execute the script every fifteen minutes. The initial five minutes was modified because the script was heavy-loading the remote IP detector. Please keep in mind not to spam the API or you will be rate limited.

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
[Install]
WantedBy=multi-user.target
[Unit]
Description=Run cfupdate.service every five minutes
[Timer]
OnCalendar=*:0/15
#!/bin/bash
# Initial fork from benkulbertis/cloudflare-update-record.sh by lifehome
# Forked from lifehome/cfupdater
# CHANGE THESE
# $Domains array should have at least two elements : ("Which record you want to be synced" "4 for IPv4 or 6 for IPv6")
declare -a Domains=("sub_or_owned.domain" "4" "sub_or_owned.domain" "6");
auth_email="email@domain.com" # The email used to login 'https://dash.cloudflare.com'
auth_key="fffffffffffffffffffffffffffffffffffff" # Top right corner, "My profile" > "Global API Key"
zone_identifier="ffffffffffffffffffffffffffffffff" # Can be found in the "Overview" tab of your domain
# Configure output interface if your ISP alternates between dual stack and IPv4 only
# If not configured, when getting at ipv6=$(curl -6 -s https://ipv6.detector), the script will hang (not getting any IPv6)
outputif="ppp0" # Output interface (also used for warn_v4)
# Optional
# Leave as is if not necessary
warn_v4=false # Warn if IPv4 is routed by your ISP
# DO NOT CHANGE LINES BELOW
# SCRIPT START
main() {
echo "[Cloudflare DDNS] Check Initiated" | systemd-cat
# Let's check connection to the Internet (Google is always online)
while :; do
check_connect=$(ping -q -w1 -c1 google.com &>/dev/null && echo online || echo offline)
if [ "$check_connect" == "online" ]; then
break
else
echo "[Cloudflare DDNS] Waiting for Internet connection" | systemd-cat
sleep 30
fi
done
#ipv4=$(curl -4 -s https://icanhazip.com/)
#ipv4=$(curl -4 -s http://plain-text-ip.com/)
#ipv4=$(curl -4 -s https://ip.seeip.org)
#ipv4=$(curl -4 -s https://ident.me/)
ipv4=$(curl -4 -s https://wtfismyip.com/text)
# Checking IPv6 on output interface (if global IPv6 present)
if [ "$(ifconfig $outputif | grep global)" == "" ]; then
ipv6="noipv6"
else
#ipv6=$(curl -6 -s https://icanhazip.com/)
#ipv6=$(curl -6 -s http://plain-text-ip.com/)
#ipv6=$(curl -6 -s https://ip.seeip.org)
#ipv6=$(curl -6 -s https://ident.me/)
ipv6=$(curl -6 -s https://wtfismyip.com/text)
fi
len=${#Domains[@]}
if [ "$len" -lt 2 ]; then
echo -e "[Cloudflare DDNS] Not enough elements in Domains array" | systemd-cat -p err
exit 1
fi
if [ $(( $len % 2)) -ne 0 ]; then
echo -e "[Cloudflare DDNS] Domains array is defined with an odd number of elements" | systemd-cat -p err
exit 1
fi
ver_err=0
for (( i=0; i < $len; i++ ))
do
record_name=${Domains[$i]}
i=$(( $i + 1 ))
ip_type=${Domains[$i]}
select_type $ip_type $record_name
if [[ "$?" -eq 1 ]]; then
ver_err=$(( $ver_err + 1 ))
fi
done
if [ "$ver_err" -gt 0 ]; then
echo -e "[Cloudflare DDNS] Errors were encountered when parsing array" | systemd-cat -p err
exit 1
fi
echo "[Cloudflare DDNS] Check Finished successfully" | systemd-cat
exit 0
}
select_type() {
# Check selected ip_type that needs to be updated
local ip=""
local record_type=""
if [ "$1" == "4" ]; then
ip=$ipv4
if [ $warn_v4 ]; then
local ipv4_local=$(ifconfig $outputif | grep -Po '(?<=inet)[^"]*(?=netmask)')
if [ $ip != $ipv4_local ]; then
echo "[Cloudflare DDNS] Warning! IPv4 is routed by the ISP" | systemd-cat -p notice
fi
fi
record_type="A"
else
ip=$ipv6
if [ "$ip" == "noipv6" ]; then
echo "[Cloudflare DDNS] Warning! No IPv6 detected on $outputif interface" | systemd-cat -p notice
return 0
fi
record_type="AAAA"
fi
# Seek for the record
local record=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$2" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json")
# Wrong login email or API key
if [[ $record == *"\"code\":9103"* ]]; then
echo -e "[Cloudflare DDNS] Unknown X-Auth-Key or X-Auth-Email!" | systemd-cat -p crit
exit 1
fi
# Wrong zone identifier
if [[ $record == *"\"code\":7003"* ]]; then
echo -e "[Cloudflare DDNS] Object (zone) identifier is invalid!" | systemd-cat -p crit
exit 1
fi
# Can't do anything without the record
if [[ $record == *"\"count\":0"* ]]; then
echo -e "[Cloudflare DDNS] Record $2 does not exist, perhaps create one first?" | systemd-cat -p crit
return 1
fi
# Set existing IP address from the fetched record
local oldip=$(echo "$record" | grep -Po '(?<="type":"'$record_type'","name":"'$2'","content":")[^"]*' | head -1)
# Compare if they're the same
if [ "$ip" == "$oldip" ]; then
echo "[Cloudflare DDNS] IP($oldip) for record $2 has not changed." | systemd-cat -p notice
return 0
fi
# The execution of update for the IP
local id_ip=$(echo "$record" | grep -Po '([^"]*)[^"]*(?=","type":"'$record_type'","name":"'$2'","content":")' | head -1)
local oldproxiable=$(echo "$record" | grep -Po '(?<="type":"'$record_type'","name":"'$2'","content":"'$oldip'","proxiable":)[^,]*' | head -1)
local oldproxied=$(echo "$record" | grep -Po '(?<="type":"'$record_type'","name":"'$2'","content":"'$oldip'","proxiable":'$oldproxiable',"proxied":)[^,]*' | head -1)
local oldttl=$(echo "$record" | grep -Po '(?<="type":"'$record_type'","name":"'$2'","content":"'$oldip'","proxiable":'$oldproxiable',"proxied":'$oldproxied',"ttl":)[^,]*' | head -1)
local update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$id_ip" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" --data "{\"id\":\"$zone_identifier\",\"type\":\"$record_type\",\"name\":\"$2\",\"content\":\"$ip\",\"ttl\":$oldttl,\"proxied\":$oldproxied}")
# The moment of truth
case "$update" in
*"\"success\":false"*)
echo -e "[Cloudflare DDNS] Update failed for $id_ip. DUMPING RESULTS:\n$update" | systemd-cat -p err
return 1;;
*)
echo "[Cloudflare DDNS] IP($1) for record $2 synced to: $ip" | systemd-cat -p notice
return 0;;
esac
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment