Skip to content

Instantly share code, notes, and snippets.

@badrianiulian
Last active May 26, 2020 10:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save badrianiulian/05fe9ee208344cfa0af3134f8ed8c7e5 to your computer and use it in GitHub Desktop.
Save badrianiulian/05fe9ee208344cfa0af3134f8ed8c7e5 to your computer and use it in GitHub Desktop.
Improved Cloudflare API v4 Dynamic DNS Update in Bash for IPv4/IPv6 records

"Cloudflare DDNS bash client with systemd"

Improved version

Initially cfupdater-badrianiulian forked from cfupdater-lifehome.

This is the new script I use to update DDNS through Cloudflare. It analyses the output interface and updates, deletes or adds records to the Cloudflare defined domain.

This script uses 'jq', 'curl', 'grep', 'ifconfig' and 'ping'

It has the same usage instructions as the previous script.

How to use in debian?

Put the cfupdater file into /usr/local/bin and run:

chmod +x /usr/local/bin/cfupdater

Copy cfupdate.service and cfupdate.timer into /etc/systemd/system/ and run:

systemctl daemon-reload
systemctl enable cfupdate.service
systemctl enable cfupdate.timer
systemctl start cfupdate.timer

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-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/cfupdater
[Install]
WantedBy=multi-user.target
[Unit]
Description=Run cfupdate.service every fifteen minutes
[Timer]
OnCalendar=*:0/15
Unit=cfupdate.service
[Install]
WantedBy=timers.target
#!/bin/bash
# CHANGE THESE
domain="example.domain" # Which domain you want to be synced
auth_email="example.domain.admin@example.domain.com" # The email used to login 'https://dash.cloudflare.com'
auth_key="112233445566778899aabbccddeeff001122a" # Top right corner, "My profile" > "Global API Key"
zone_identifier="ffeeddccbbaa99887766554433221100" # Can be found in the "Overview" tab of your domain
outputif="ppp0"
# Optional
# When domain is proxied by Cloudflare ttl is 1 (set default by Cloudflare)
ttl=1
proxied=true
# 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
sleep 3
# Checking IPv4 on output interface (if IPv4 is not behind a router)
ipv4_local=$(ifconfig $outputif | grep -Po '(?<=inet )[^"]*(?= netmask)')
#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)
if [[ $ipv4 != "$ipv4_local" ]]; then
echo "[Cloudflare DDNS] Warning! IPv4 is routed by the ISP" | systemd-cat -p notice
ipv4="noipv4"
fi
# Checking IPv6 on output interface (if global IPv6 present)
ipv6_local=$(ifconfig $outputif | grep global | grep -Po '(?<=inet6 )[^"]*(?= prefixlen)')
if [ "$ipv6_local" == "" ]; then
echo "[Cloudflare DDNS] Warning! No IPv6 detected on $outputif interface" | systemd-cat -p notice
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
if [ "$ipv4" == "noipv4" ] && [ "$ipv6" == "noipv6" ]; then
echo -e "[Cloudflare DDNS] Cannot update any records (IPv4 or IPv6)" | systemd-cat -p err
exit 1
fi
record=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$domain" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json")
ver_err=0
if [ "$ipv4" != "noipv4" ]; then
insupd_record "4"
else
delete_record "4"
fi
if [ "$ipv6" != "noipv6" ]; then
insupd_record "6"
else
delete_record "6"
fi
if [ "$ver_err" -gt 0 ]; then
echo -e "[Cloudflare DDNS] Errors were encountered" | systemd-cat -p err
exit 1
fi
echo "[Cloudflare DDNS] Check finished successfully" | systemd-cat
exit 0
}
insupd_record() {
local record_type
local ip
if [ "$1" == "4" ]; then
record_type="A"
ip=$ipv4
else
record_type="AAAA"
ip=$ipv6
fi
local id_ip
id_ip=$(echo "$record" | jq -j '.result[] | select(.type == "'$record_type'") | .id' | head -1)
if [ "$id_ip" != "" ]; then
# Set existing IP address from the fetched record
local oldip
oldip=$(echo "$record" | jq -j '.result[] | select(.type == "'$record_type'") | .content' | head -1)
# Compare if they're the same
if [ "$ip" == "$oldip" ]; then
echo "[Cloudflare DDNS] IP($oldip) for record $domain has not changed." | systemd-cat -p notice
return 0
fi
local update
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\":\"$domain\",\"content\":\"$ip\",\"ttl\":$ttl,\"proxied\":$proxied}")
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 -e "[Cloudflare DDNS] IP $ip was updated for $domain" | systemd-cat -p notice
return 0;;
esac
else
local insert
insert=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" --data "{\"type\":\"$record_type\",\"name\":\"$domain\",\"content\":\"$ip\",\"ttl\":$ttl,\"proxied\":$proxied}")
case "$insert" in
*"\"success\":false"*)
echo -e "[Cloudflare DDNS] Insert failed for $ip. DUMPING RESULTS:\n$insert" | systemd-cat -p err
return 1;;
*)
echo -e "[Cloudflare DDNS] IP $ip was inserted for $domain" | systemd-cat -p notice
return 0;;
esac
fi
}
delete_record() {
local record_type
local ip
if [ "$1" == "4" ]; then
record_type="A"
ip=$ipv4
else
record_type="AAAA"
ip=$ipv6
fi
local id_ip
id_ip=$(echo "$record" | jq -j '.result[] | select(.type == "'$record_type'") | .id' | head -1)
if [ "$id_ip" != "" ]; then
delete=$(curl -s -X DELETE "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")
case "$delete" in
*"\"success\":false"*)
echo -e "[Cloudflare DDNS] Delete failed for $id_ip. DUMPING RESULTS:\n$delete" | systemd-cat -p err
return 1;;
*)
echo -e "[Cloudflare DDNS] IP $ip was deleted for $domain" | systemd-cat -p notice
return 0;;
esac
fi
return 0
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment