-
-
Save bigheadsmith/b7bd145be21dd0b9f382babbbeea9685 to your computer and use it in GitHub Desktop.
[Unit] | |
Description=Cloudflare DDNS service | |
After=network.target | |
[Service] | |
Type=oneshot | |
ExecStart=/usr/local/bin/cfupdater/cfupdater.sh | |
[Install] | |
WantedBy=multi-user.target |
#!/bin/bash | |
# GET CONFIG PATH | |
configjson="$( cd "$(dirname "$0")" ; pwd -P )/config.json" | |
# GET INTERFACE FROM CONFIG FILE | |
interface=$(jq -r '.interface' $configjson) | |
# GET AUTH VALUES FROM CONFIG FILE | |
api_tok=$(jq -r '.api_tok' $configjson) | |
global_key=$(jq -r '.global_key' $configjson) | |
auth_email=$(jq -r '.auth_email' $configjson) | |
# GET IPv4 ADDRESS | |
ipv4=$(curl -4 -s -m 60 --interface $interface https://wtfismyip.com/text) | |
# CHECK IPv4 ADDRESS EXISTS | |
if [ -z "$ipv4" ]; then | |
ipv4="noipv4" | |
echo -e "[Cloudflare DDNS] No IPv4 address found for interface $interface! Please check connection and interface in config.json" | systemd-cat -p notice | |
fi | |
# GET IPv6 ADDRESS | |
ipv6=$(curl -6 -s -m 60 --interface $interface https://wtfismyip.com/text) | |
# CHECK IPv6 ADDRESS EXISTS | |
if [ -z "$ipv6" ]; then | |
ipv6="noipv6" | |
echo -e "[Cloudflare DDNS] No IPv6 address found for interface $interface! Please check connection and interface in config.json" | systemd-cat -p notice | |
fi | |
# IF IPv4 & IPv6 NOT FOUND THEN ASSUME NOT CONNECTED TO INTERNET | |
if [ "$ipv4" == "noipv4" ] && [ "$ipv6" == "noipv6" ]; then | |
echo -e "[Cloudflare DDNS] No internet connectivity on interface $interface! Please check connection and interface in config.json" | systemd-cat -p crit | |
exit 1 | |
fi | |
# MAKE AUTH_HEADERS | |
if [ -z "$api_tok" ]; then | |
declare -a auth_headers=('-H' "X-Auth-Email: $auth_email" '-H' "X-Auth-Key: $global_key" '-H' "Content-Type: application/json") | |
else | |
declare -a auth_headers=('-H' "Authorization: Bearer $api_tok" '-H' "Content-Type: application/json") | |
fi | |
# GET ALL ZONES FROM API | |
api_zones=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones" "${auth_headers[@]}") | |
# CHECK NO ERROR WITH AUTH_HEADERS | |
if [[ $api_zones = *"\"code\":6003"* ]]; then | |
echo -e "[Cloudflare DDNS] Unknown Api Token, Global API Key or Email! Check config.json" | systemd-cat -p crit | |
exit 1 | |
fi | |
# GET ALL ZONES FROM CONFIG FILE | |
declare -a config_zones=($(jq -r '.zones[] | .zone' $configjson)) | |
# CHECK CONFIG ZONES LOADED | |
lenz=${#config_zones[@]} | |
if [ "$lenz" == 0 ]; then | |
echo -e "[Cloudflare DDNS] No Zones in config file! Check config.json" | systemd-cat -p crit | |
exit 1 | |
fi | |
# LOOP OVER CONFIG ZONES | |
for (( z=0; z < $lenz; z++ )) | |
do | |
# CHECK CONFIG ZONE EXISTS ON CLOUDFLARE | |
config_zone_name=${config_zones[$z]} | |
api_zone_id=$(echo $api_zones | jq -r --arg zone $config_zone_name '.result[] | select(.name==$zone) | "\(.id)"') | |
if [ "$api_zone_id" == "" ]; then | |
echo -e "[Cloudflare DDNS] Zone $config_zone_name invalid! Check spelling in config.json or that it exists on your Cloudflare account" | systemd-cat -p crit | |
continue | |
fi | |
# GET ALL RECORDS FOR CONFIG ZONE | |
declare -a config_records=($(jq -r --arg zone $config_zone_name '.zones[] | select(.zone==$zone) | .records[] | .record + " " + .type' $configjson)) | |
# CHECK CONFIG RECORDS LOADED | |
lenr=${#config_records[@]} | |
if [ "$lenr" == 0 ]; then | |
echo -e "[Cloudflare DDNS] No records in config file for ${config_zones[$z]}! Check config.json" | systemd-cat -p crit | |
continue | |
fi | |
# GET ALL API RECORDS FOR CONFIG ZONE | |
api_records=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$api_zone_id/dns_records" "${auth_headers[@]}") | |
# LOOP OVER CONFIG RECORDS | |
for (( r=0; r < $lenr; r++ )) | |
do | |
# CHECK IF CONFIG RECORD IS ROOT OR SUBDOMAIN | |
if [ "${config_records[$r]}" == "@" ]; then | |
config_record_name=$config_zone_name | |
else | |
config_record_name="${config_records[$r]}.$config_zone_name" | |
fi | |
r=$(( $r + 1 )) | |
type=${config_records[$r]} | |
# SET CURRENT RECORD TYPE TO CHECK | |
if [ "$type" == "A" ]; then | |
# FOR A RECORDS, CHECK IPv4 AVAILABLE | |
if [ "$ipv4" == "noipv4" ]; then | |
echo "[Cloudflare DDNS] Warning! No IPv4 detected. Cannot update A (IPv4) record for $config_record_name" | systemd-cat -p crit | |
continue | |
fi | |
ip=$ipv4 | |
elif [ "$type" == "AAAA" ]; then | |
# FOR AAAA RECORDS, CHECK IPv6 AVAILABLE | |
if [ "$ipv6" == "noipv6" ]; then | |
echo "[Cloudflare DDNS] Warning! No IPv6 detected. Cannot update AAAA (IPv6) record for $config_record_name" | systemd-cat -p crit | |
continue | |
fi | |
ip=$ipv6 | |
else | |
echo "[Cloudflare DDNS] Warning! Record type \"$type\" not valid. Check config.json" | systemd-cat -p crit | |
continue | |
fi | |
# CHECK CONFIG RECORD EXISTS ON CLOUDFLARE | |
api_record_id=$(echo "$api_records" | jq -r --arg record $config_record_name --arg type $type '.result[] | select(.name==$record and .type==$type) | "\(.id)"') | |
if [ "$api_record_id" == "" ]; then | |
echo -e "[Cloudflare DDNS] Record $config_record_name invalid! Check spelling in config.json or that it exists on your Cloudflare account" | systemd-cat -p crit | |
continue | |
fi | |
# GET API RECORD IP ADDRESS | |
api_record_ip=$(echo "$api_records" | jq -r --arg id $api_record_id '.result[] | select(.id==$id) | "\(.content)"') | |
# CHECK IF API RECORD IP IS DIFFERENT TO CURRENT IP | |
if [ "$ip" == "$api_record_ip" ]; then | |
echo "[Cloudflare DDNS] IP ($ip) for record $config_record_name has not changed." | systemd-cat -p notice | |
continue | |
else | |
proxied=$(echo "$api_records" | jq -r --arg id $api_record_id '.result[] | select(.id==$id) | "\(.proxied)"') | |
ipupdate=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$api_zone_id/dns_records/$api_record_id" "${auth_headers[@]}" --data "{\"type\":\"$type\",\"name\":\"$config_record_name\",\"content\":\"$ip\",\"proxied\":$proxied}") | |
fi | |
# CHECK STATUS OF UPDATE | |
case "$ipupdate" in | |
*"\"success\":false"*) | |
echo -e "[Cloudflare DDNS] Update failed for $config_record_name. DUMPING RESULTS:\n$ipupdate" | systemd-cat -p err;; | |
*) | |
echo "[Cloudflare DDNS] $type record for $config_record_name synced to $ip" | systemd-cat -p notice;; | |
esac | |
done | |
done |
[Unit] | |
Description=Run cfupdater.service every fifteen minutes | |
[Timer] | |
OnCalendar=*:0/15 | |
[Install] | |
WantedBy=timers.target |
# Cloudflare DDNS bash client with systemd | |
This is a bash script to act as a Cloudflare DDNS client, useful replacement for ddclient. | |
# Requirements | |
- curl | |
- jq | |
# How to use? | |
1) Move `cfupdater.sh` to `/usr/local/bin/cfupdater`. | |
``` | |
mkdir -p /usr/local/bin/cfupdater && mv cfupdater.sh /usr/local/bin/cfupdater | |
``` | |
2) Make `cfupdater.sh` executable | |
``` | |
chmod +x /usr/local/bin/cfupdater/cfupdater.sh | |
``` | |
3) Edit config in config.json | |
``` | |
# Interface to get IP address for (use ifconfig to find interface name) | |
"interface": "eth0" | |
# Create an API Token for DNS updates only (more secure than Global Key) | |
# Leave empty to use Global API Token | |
"api_tok": "XXXXXXXXXXXXXXXXXXXX" | |
# OR use Global API Key (less secure) and Cloudflare login email address | |
"global_key": "XXXXXXXXXXXXXXXXXXXX", | |
"auth_email": "test@example.com" | |
# Zones and records to update. | |
# Use "@" record to update base domain IP address | |
# Record type A for IPv4 or AAAA for IPv6 | |
# Must have comma (,) after closing } each zone and record except last | |
"zones" : [ | |
{ | |
"zone" : "example.com", | |
"records" : [ | |
{ "record" : "@", "type" : "A" }, | |
{ "record" : "www", "type" : "A" }, | |
{ "record" : "www", "type" : "AAAA" } | |
] | |
}, | |
{ | |
"zone" : "example2.com", | |
"records" : [ | |
{ "record" : "sub1", "type" : "A" }, | |
{ "record" : "sub1", "type" : "AAAA" }, | |
{ "record" : "sub2", "type" : "A" } | |
] | |
} | |
] | |
``` | |
4) Move config to **the same location as cfupdater.sh** | |
``` | |
mv config.json /usr/local/bin/cfupdater | |
``` | |
5) Create a systemd service unit at `/etc/systemd/system/`, `cfupdater.service` is shown as an example. | |
``` | |
mv cfupdater.service /etc/systemd/system/ | |
``` | |
6) Create a systemd timer unit at **the same location of the service unit**, `cfupdater.timer` is shown as an example. | |
``` | |
mv cfupdater.timer /etc/systemd/system/ | |
``` | |
7) Enable and start systemd service | |
``` | |
systemctl enable cfupdater.timer && systemctl start cfupdater.timer | |
``` | |
8) Output can be seen by typing | |
``` | |
journalctl -f | |
``` | |
# Note | |
The default `cfupdater.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 |
{ | |
"interface": "eth0", | |
"api_tok": "XXXXXXXXXX", | |
"global_key": "XXXXXXXXXX", | |
"auth_email": "CLOUDFLARE EMAIL ADDRESS", | |
"zones" : [ | |
{ | |
"zone" : "example.com", | |
"records" : [ | |
{ "record" : "@", "type" : "A" }, | |
{ "record" : "www", "type" : "A" }, | |
{ "record" : "www", "type" : "AAAA" } | |
] | |
}, | |
{ | |
"zone" : "example2.com", | |
"records" : [ | |
{ "record" : "sub1", "type" : "A" }, | |
{ "record" : "sub1", "type" : "AAAA" }, | |
{ "record" : "sub2", "type" : "A" } | |
] | |
} | |
] | |
} |
Also do you removed proxied value? Because by default Cloudflare sets it to
true
. Some of my domains needs to be DNS only. That's why can not remove proxied value.
Maybe Cloudflare have changed how they do this. I have a combination of proxied and DNS only and the proxied setting is not changed on any records from what it is already set as on the Cloudflared dashboard.
For example, the two records that got updated in the log above are DNS only and have remained as DNS only after the IP sync
Also do you removed proxied value? Because by default Cloudflare sets it to
true
. Some of my domains needs to be DNS only. That's why can not remove proxied value.Maybe Cloudflare have changed how they do this. I have a combination of proxied and DNS only and the proxied setting is not changed on any records from what it is already set as on the Cloudflared dashboard.
For example, the two records that got updated in the log above are DNS only and have remained as DNS only after the IP sync
Whoa! Let me check it! Will knock you ASAP!
NOPE!
Just tried out. Now it's reversed! If I use this, {\"id\":\"$zone_id\",\"type\":\"$record_type\",\"name\":\"$record_name\",\"content\":\"$cur_ip\",\"proxied\":$proxied_value}
then proxied value gets placed as the variable value. But if I use this,
{\"id\":\"$zone_id\",\"type\":\"$record_type\",\"name\":\"$record_name\",\"content\":\"$cur_ip\"}
then proxied value gets placed as DNS only. Previously Cloudflare sets it to Proxied.
NOPE!
Just tried out. Now it's reversed! If I use this,{\"id\":\"$zone_id\",\"type\":\"$record_type\",\"name\":\"$record_name\",\"content\":\"$cur_ip\",\"proxied\":$proxied_value}
then proxied value gets placed as the variable value. But if I use this,
{\"id\":\"$zone_id\",\"type\":\"$record_type\",\"name\":\"$record_name\",\"content\":\"$cur_ip\"}
then proxied value gets placed as DNS only. Previously Cloudflare sets it to Proxied.
Good spot, thanks! I've added a line to get current proxied value and use in the update to stop it from changing
No prob! 😉
Tried API Token method. It's still in BETA that's why encountered with many bugs and glitches. Also it's not as fast as Global API method. Sometime it's unable to fetch JSON data. For me it's too much unreliable. I am sticking with Global API method for now. It will be a nice security measure after it finishes BETA phase.
Also do you removed proxied value? Because by default Cloudflare sets it to
true
. Some of my domains needs to be DNS only. That's why can not remove proxied value.