Last active
February 5, 2025 16:40
-
-
Save stuudmuffin/81c44601534b8c0b136eace6fe324054 to your computer and use it in GitHub Desktop.
Update Cloudflare DNS entries for Homelab
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# requirements: apt install jq curl sed awk tr | |
# | |
# This script does a few limited things. | |
# 1: when run with ./cloudflare.sh {IP} auto | |
# it will take the IP address, and attempt to apply it to all dns record IDS listed in $domain_data | |
# 2: help you get the Zone ID's and Record ID's needed for domain_data, or manually update one domains dns records | |
# | |
# When running with no arguments `./cloudflare.sh` you'll be presented with 2 inputs. | |
# 1: Will ask which domain you wish to work with (manual runs only work with 1 domain per script execution) | |
# 2: Choose between "find zone ID, retrieve DNS records, and update DNS records" | |
# | |
# To get started, all you need is the api tokens for your domains in their respective files, and the domain name in this | |
# $domain_data json blob | |
# option 1 of the cloudflare actions will use this domain name to find the zone id | |
# This can then be put into its respective place in domain_data | |
# option 2 of the cloudflare actions will use the zone_id saved in the previous step to get all A and AAAA records | |
# NOTE I haven't added any logic to actually handle replacing AAAA records. only ipv4 | |
# option 3 of the cloudflare actions will use the zone_id, and saved dns record IDs to get the current record information. | |
# when run with `./cloudflare 1.2.3.4` and then selecting option 3, the record_list ids of your select domain will attempt | |
# to update only those dns records with the ip 1.2.3.4 | |
# | |
# once the domain_data information is all filled out, you can run `./cloudflare.sh 1.2.3.4 auto` to update all record_list | |
# ids to the new ip. I've combined this with automation in my home router (mikrotik) to trigger cloudflare.sh when the | |
# router detects it has a new IP. (see update_dns_mikrotik_script file for this logic) | |
new_ip=$1 | |
auto_run=$2 | |
# I uncomment this for debugging before fully allowing the script to be remotely triggered | |
#echo "hello $new_ip. you will $auto_run" | |
#exit | |
# accepting the format `dns_cloudflare_api_token = token` with nothing else in the file | |
# I am using this file for letsencrypt to aid in ssl generation, and I didn't want duplicate API token locations | |
dns_cloudflare_api_token_example1=$(sed 's/[[:space:]]*=[[:space:]]*/=/g' /etc/ssl/private/example1.ini | awk -F= '{print $2}' | tr -d '[:space:]') | |
dns_cloudflare_api_token_example2=$(sed 's/[[:space:]]*=[[:space:]]*/=/g' /etc/ssl/private/example2.ini | awk -F= '{print $2}' | tr -d '[:space:]') | |
# Dynamically create the domain data JSON string with expanded variables | |
domain_data=$(jq -n \ | |
--arg example_token1 "$dns_cloudflare_api_token_example1" \ | |
--arg example_token2 "$dns_cloudflare_api_token_example2" \ | |
'{ | |
"example1.com": { | |
token: $example_token1, | |
zone_id: "example9f86d081884c7d659a2feaa0c5", | |
record_list: [ "example60303ae22b998861bce3b28", "examplefd61a03af4f77d870fc2" ] | |
}, | |
"example2.com": { | |
token: $example_token2, | |
zone_id: "examplea4e624d686e03ed2767c0abd8", | |
record_list: [ "examplea140c0c1eda2def2b830363b" ] | |
} | |
}') | |
# Build list of domains bases on domain_data | |
#domains=("example1.com" "example2.com") | |
domains=($(echo "$domain_data" | jq -r 'keys[]')) | |
# Function to interact with the Cloudflare API | |
cloudflare_api_call() { | |
local api_token=$1 # First arugment is the api token | |
local api_path=$2 # Second argument is the API path (empty when doing zone id lookup) | |
local data=$3 # Third argument is optional data (for PUT requests or similar) | |
# Base URL for Cloudflare API | |
local api_url="https://api.cloudflare.com/client/v4/zones$api_path" | |
# If data is provided, use it in a PUT; otherwise, use a GET request | |
if [[ -n "$data" ]]; then | |
# POST request with data | |
response=$(curl -s -X PUT "$api_url" \ | |
-H "Authorization: Bearer $api_token" \ | |
-H "Content-Type: application/json" \ | |
--data "$data") | |
else | |
# GET request (no data) | |
response=$(curl -s -X GET "$api_url" \ | |
-H "Authorization: Bearer $api_token" \ | |
-H "Content-Type: application/json") | |
fi | |
# Return the response (could be JSON or status code, etc.) | |
echo "$response" | |
} | |
# Function to validate the update data | |
validate_update_data() { | |
local update_data="$1" | |
# 1. Validate "type" - it should be either A or AAAA | |
type=$(echo "$update_data" | jq -r '.type') | |
if [[ ! "$type" =~ ^(A|AAAA)$ ]]; then | |
echo "Invalid type. It should be 'A' or 'AAAA'." | |
return 1 # Return false | |
fi | |
# 2. Validate "name" - it should be a valid domain name (basic validation) | |
name=$(echo "$update_data" | jq -r '.name') | |
if [[ ! "$name" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then | |
echo "Invalid domain name format: $name" | |
return 1 # Return false | |
fi | |
# 3. Validate "content" - it should be a valid IP address (IPv4 validation) | |
content=$(echo "$update_data" | jq -r '.content') | |
if [[ ! "$content" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then | |
echo "Invalid IP address format: $content" | |
return 1 # Return false | |
fi | |
# Check if each octet is in the range 0-255 | |
IFS='.' read -r -a octets <<< "$content" | |
for octet in "${octets[@]}"; do | |
if [[ $octet -gt 255 || $octet -lt 0 ]]; then | |
echo "IP address octet out of range: $octet" | |
return 1 # Return false | |
fi | |
done | |
# 4. Validate "ttl" - it should be a number | |
ttl=$(echo "$update_data" | jq -r '.ttl') | |
if [[ ! "$ttl" =~ ^[0-9]+$ ]]; then | |
echo "Invalid ttl. It should be a number." | |
return 1 # Return false | |
fi | |
# 5. Validate "proxied" - it should be true or false | |
proxied=$(echo "$update_data" | jq -r '.proxied') | |
if [[ ! "$proxied" =~ ^(true|false)$ ]]; then | |
echo "Invalid proxied value. It should be true or false." | |
return 1 # Return false | |
fi | |
return 0 # Return true | |
} | |
# This is in a funciton so it best handles being run manually, or automatically | |
cloudflare_update_dns() { | |
local api_token=$1 # First arugment is the api token | |
local domain=$2 # Second argument is the domain | |
local new_ip=$3 # the new ip for our payload | |
# Get our zone_id and record_list from our data_domain array | |
local zone_id=$(echo "$domain_data" | jq -r ".\"$domain\".zone_id") | |
local record_list | |
mapfile -t record_list < <(echo "$domain_data" | jq -r ".\"$domain\".record_list[]?") | |
# handle empty record_list cases | |
if [ ${#record_list[@]} -eq 0 ]; then | |
echo "No records found in record_list for $domain. Skipping..." | |
echo "" | |
return | |
fi | |
# Echoing the full record list for logging | |
echo "Processing record list ${record_list[@]}" | |
for cur_record_id in "${record_list[@]}"; do | |
echo "Gathering data for $cur_record_id" | |
response=$(cloudflare_api_call "$api_token" "/$zone_id/dns_records/$cur_record_id") | |
# Handle api call failiures | |
success=$(echo "$response" | jq -r '.success') | |
if [ "$success" != "true" ]; then | |
echo "API call failed. Exiting..." | |
echo $response | |
exit 1 | |
else | |
echo "$response" | jq -r '"Record found for \(.result.name)"' | |
fi | |
# Lets not update the record if we don't need to | |
cur_ip=$(echo "$response" | jq -r '.result.content') | |
if [[ "$cur_ip" == "$new_ip" ]]; then | |
echo "Record already matches the desired ip $cur_ip" | |
echo "" | |
continue | |
fi | |
# if script is started without an IP, then we'll just report the current record_list data | |
if [ -z "$new_ip" ]; then | |
update_data=$(echo "$response" | jq -r '{"type": .result.type, "name": .result.name, "content": .result.content, "ttl": .result.ttl, "proxied": .result.proxied}') | |
echo "$update_data" | |
else | |
# inject our new ip | |
update_data=$(echo "$response" | jq -r --arg new_ip "$new_ip" '{"type": .result.type, "name": .result.name, "content": $new_ip, "ttl": .result.ttl, "proxied": .result.proxied}') | |
fi | |
# Call the validation function. Don't want to try submitting malformed data | |
if ! validate_update_data "$update_data"; then | |
echo "Validation failed! Quitting..." | |
exit | |
fi | |
if [ -n "$new_ip" ]; then | |
echo "Updating record..." | |
response=$(cloudflare_api_call "$api_token" "/$zone_id/dns_records/$cur_record_id" "$update_data") | |
# Exit if an update fails, end echo the failed response | |
success=$(echo "$response" | jq -r '.success') | |
if [ "$success" != "true" ]; then | |
echo "API call failed. Exiting..." | |
echo $response | |
exit 1 | |
else | |
echo "$response" | jq -r '"Success for \(.result.name)"' | |
fi | |
fi | |
echo "" | |
done | |
} | |
# Handling for autonomus runs | |
if [ -n "$auto_run" ] && [ "$auto_run" == "auto" ]; then | |
for cur_domain in "${domains[@]}"; do | |
cur_token=$(echo "$domain_data" | jq -r ".\"$cur_domain\".token") | |
echo "Updating relevent records for $cur_domain - $cur_token" | |
cloudflare_update_dns "$cur_token" "$cur_domain" "$new_ip" | |
done | |
exit | |
fi | |
# Hanlding for manual runs | |
while true; do | |
# Define your domains and prompt for selection | |
echo "Select the domain you want to check:" | |
for i in "${!domains[@]}"; do | |
echo "$((i + 1))) ${domains[$i]}" | |
done | |
read -p "Enter the number of the domain you want to check: " choice | |
# Check if the choice is a valid index, and set cur_domain | |
if [[ "$choice" -ge 1 && "$choice" -le ${#domains[@]} ]]; then | |
cur_domain="${domains[$((choice - 1))]}" | |
echo "You selected $cur_domain" | |
break | |
else | |
# If invalid, display an error | |
echo "Invalid selection! Please choose a valid number." | |
fi | |
done | |
cur_token=$(echo "$domain_data" | jq -r ".\"$cur_domain\".token") | |
cur_zoid=$(echo "$domain_data" | jq -r ".\"$cur_domain\".zone_id") | |
mapfile -t cur_reli < <(echo "$domain_data" | jq -r ".\"$cur_domain\".record_list[]?") | |
# Example of accessing and displaying the extracted values | |
echo "Selected domain token: $cur_token" | |
echo "Selected domain zone_id: $cur_zoid" | |
echo "Selected domain reli: ${cur_reli[@]}" | |
while true; do | |
# Define your api functions | |
echo "Select the function you wish to perform:" | |
echo "1) get zone id" | |
echo "2) get dns records" | |
echo "3) update dns record" | |
# Prompt user for domain choice | |
read -p "Function number: " choice | |
# Use a case statement to handle the domain selection | |
case $choice in | |
1) | |
# Example: Call Cloudflare API to get DNS Zone ID | |
echo "Getting Zone ID..." | |
response=$(cloudflare_api_call "$cur_token" "" | jq -r '.result[0].id') | |
echo "Response: $response" | |
break 1 | |
;; | |
2) | |
# Example: Call Cloudflare API to get DNS records (no data needed) | |
echo "Getting DNS records..." | |
response=$(cloudflare_api_call "$cur_token" "/$cur_zoid/dns_records") | |
records=() | |
# Initialize an empty array to hold the results | |
records=() | |
# Use jq to filter A and AAAA records and store them in the array | |
while IFS= read -r record; do | |
records+=("$record") | |
done < <(echo "$response" | jq -r '.result[] | select(.type == "A" or .type == "AAAA") | "\(.id) - \(.type) record for \(.name): \(.content)"') | |
# Loop over the array and display the records | |
echo "Displaying A and AAAA records:" | |
for record in "${records[@]}"; do | |
echo "$record" | |
done | |
break 1 | |
;; | |
3) | |
if [ -z "$new_ip" ]; then | |
echo "no new ip set. Only reporting current records" | |
fi | |
echo "Updating relevent records for this zone" | |
cloudflare_update_dns "$cur_token" "$cur_domain" "$new_ip" | |
break 1 | |
;; | |
4) | |
# Loop over the list of DNS record IDs | |
for cur_record_id in "${cur_reli[@]}"; do | |
echo "Gathering data for $cur_record_id" | |
response=$(cloudflare_api_call "$cur_token" "/$cur_zoid/dns_records/$cur_record_id") | |
success=$(echo "$response" | jq -r '.success') | |
if [ "$success" != "true" ]; then | |
echo "API call failed. Exiting..." | |
echo $response | |
exit 1 | |
else | |
echo "$response" | jq -r '"Record found for \(.result.name)"' | |
fi | |
if [ -z "$new_ip" ]; then | |
update_data=$(echo "$response" | jq -r '{"type": .result.type, "name": .result.name, "content": .result.content, "ttl": .result.ttl, "proxied": .result.proxied}') | |
else | |
update_data=$(echo "$response" | jq -r --arg new_ip "$new_ip" '{"type": .result.type, "name": .result.name, "content": $new_ip, "ttl": .result.ttl, "proxied": .result.proxied}') | |
fi | |
# Call the validation function | |
if ! validate_update_data "$update_data"; then | |
echo "Validation failed! Quitting..." | |
exit | |
fi | |
if [ -n "$new_ip" ]; then | |
echo "Skipping Updating the record..." | |
# since this script is meant to be run automatically, I don't want to accidently cause problems if I test | |
# a manual run. Uncomment this if you want to debug | |
# echo "Updating record..." | |
# response=$(cloudflare_api_call "$cur_token" "/$cur_zoid/dns_records/$cur_record_id" "$update_data") | |
# Exit if an update fails, end echo the failed response | |
success=$(echo "$response" | jq -r '.success') | |
if [ "$success" != "true" ]; then | |
echo "API call failed. Exiting..." | |
echo $response | |
exit 1 | |
else | |
echo "$response" | jq -r '"Success for \(.result.name)"' | |
fi | |
fi | |
echo "" | |
exit | |
done | |
break 1 | |
;; | |
*) | |
# Default case for invalid input | |
echo "Invalid selection! Please choose a valid number." | |
;; | |
esac | |
done |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
dns_cloudflare_api_token = exampleU3a_7jPDVj3_ujdevaa140c0c |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Simple mikrotik script to ssh-exec the cloudflare.sh file on a remote server. | |
# runs on a schedule to periodically check if our public ip has changed | |
# ip lookup includes the /subnet. for my purposes I want this removed | |
:local currentipsub [/ip address get [find interface=ether1] address]; | |
:local currentip [:pick $currentipsub 0 [:find $currentipsub "/"]]; | |
:local lastip [/file get last_ip.txt contents]; | |
# Check if the IP address has changed | |
:if ($currentip != $lastip) do={ | |
# Do something when the IP changes (e.g., log, send email, etc.) | |
/log info message="IP address changed from $lastip to $currentip"; | |
# Store the new IP to the file | |
/file set last_ip.txt contents=$currentip; | |
# Phone a friend to update DNS entries | |
/system/ssh-exec address=192.168.1.2.com user=ubuntu command="~/scripts/cloudflare.sh $currentip auto" | |
} | |
# else={ | |
# These lines are here just for debugging | |
# /log info message="IP address currently $currentip. no change"; | |
# /system/ssh-exec address=192.168.1.2.com user=ubuntu command="~/scripts/cloudflare.sh $currentip auto" | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment