Skip to content

Instantly share code, notes, and snippets.

@Angus-Moore-Dev
Created December 28, 2025 00:10
Show Gist options
  • Select an option

  • Save Angus-Moore-Dev/88129a020b1e627ad90c0b7be0224cf1 to your computer and use it in GitHub Desktop.

Select an option

Save Angus-Moore-Dev/88129a020b1e627ad90c0b7be0224cf1 to your computer and use it in GitHub Desktop.
Cloudflare DDNS Updater
#!/bin/bash
# Cloudflare Dynamic DNS Updater with Proxy
# Creates and updates DNS records to point to current public IP
# Handles wildcard, root domain, and specific subdomains
# Configuration
CLOUDFLARE_API_TOKEN="API_TOKEN"
ZONE_ID="ZONE_ID_TOKEN"
DOMAIN="domaingoeshere.com"
# Domains to create/manage
# Format: "domain_name:should_be_proxied (boolean)"
DOMAINS=(
"example.com:true" # Root domain - proxied
"*.example.com:true" # Wildcard - proxied
"minecraft.example.com:false" # Minecraft - NOT proxied (raw TCP)
"zomboid.example.com:false" # Zomboid - NOT proxied (raw TCP)
)
RECORD_TYPE="A"
TTL=1 # Auto TTL (Cloudflare decides)
LOG_FILE="/var/log/cloudflare-dns-update.log"
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
get_public_ip() {
local ip
ip=$(curl -s --max-time 10 ifconfig.me || \
curl -s --max-time 10 icanhazip.com || \
curl -s --max-time 10 checkip.amazonaws.com)
if [[ $ip =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "$ip"
else
return 1
fi
}
get_dns_record_by_name() {
local record_name="$1"
log_message "Checking if record exists: $record_name"
response=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=$RECORD_TYPE&name=$record_name" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json")
echo "$response"
}
create_dns_record() {
local record_name="$1"
local ip="$2"
local proxied="$3"
log_message "Creating new DNS record: $record_name -> $ip (proxied: $proxied)"
response=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"type\":\"$RECORD_TYPE\",\"name\":\"$record_name\",\"content\":\"$ip\",\"ttl\":$TTL,\"proxied\":$proxied}")
if echo "$response" | grep -q '"success":true'; then
log_message "✓ SUCCESS: Created $record_name"
return 0
else
log_message "✗ FAILED: Could not create $record_name"
log_message "Response: $response"
return 1
fi
}
update_dns_record() {
local record_id="$1"
local record_name="$2"
local new_ip="$3"
local proxied="$4"
log_message "Updating DNS record: $record_name (ID: $record_id) -> $new_ip (proxied: $proxied)"
response=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$record_id" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"type\":\"$RECORD_TYPE\",\"name\":\"$record_name\",\"content\":\"$new_ip\",\"ttl\":$TTL,\"proxied\":$proxied}")
if echo "$response" | grep -q '"success":true'; then
log_message "✓ SUCCESS: Updated $record_name"
return 0
else
log_message "✗ FAILED: Could not update $record_name"
log_message "Response: $response"
return 1
fi
}
process_domain() {
local domain_entry="$1"
local current_ip="$2"
# Parse domain:proxied format
local record_name="${domain_entry%:*}"
local should_proxy="${domain_entry#*:}"
log_message "=========================================="
log_message "Processing: $record_name"
log_message "Should be proxied: $should_proxy"
# Check if record exists
check_response=$(get_dns_record_by_name "$record_name")
if ! echo "$check_response" | grep -q '"success":true'; then
log_message "ERROR: Failed to check record existence"
log_message "Response: $check_response"
return 1
fi
# Check if we got any results
result_count=$(echo "$check_response" | grep -o '"id":"[^"]*"' | wc -l)
if [[ $result_count -eq 0 ]]; then
log_message "Record does not exist, creating it..."
create_dns_record "$record_name" "$current_ip" "$should_proxy"
else
log_message "Record exists, checking if update needed..."
# Extract current record details
record_id=$(echo "$check_response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
record_ip=$(echo "$check_response" | grep -o '"content":"[^"]*"' | head -1 | cut -d'"' -f4)
record_proxied=$(echo "$check_response" | grep -o '"proxied":[^,}]*' | head -1 | cut -d':' -f2 | tr -d ' ')
log_message "Current values: IP=$record_ip, Proxied=$record_proxied"
log_message "Desired values: IP=$current_ip, Proxied=$should_proxy"
# Check if update needed
if [[ "$record_ip" != "$current_ip" ]] || [[ "$record_proxied" != "$should_proxy" ]]; then
log_message "Update required!"
update_dns_record "$record_id" "$record_name" "$current_ip" "$should_proxy"
else
log_message "✓ No update needed - record is already correct"
fi
fi
}
main() {
log_message "=========================================="
log_message "Cloudflare DNS Manager - Create/Update"
log_message "=========================================="
# Ensure log file exists
touch "$LOG_FILE" 2>/dev/null || {
echo "ERROR: Cannot create log file $LOG_FILE"
exit 1
}
log_message "Configuration:"
log_message " Domain: $DOMAIN"
log_message " Zone ID: $ZONE_ID"
log_message " Managing ${#DOMAINS[@]} DNS records"
# Get current public IP
log_message "Getting current public IP..."
current_ip=$(get_public_ip)
if [[ -z "$current_ip" ]]; then
log_message "FATAL ERROR: Failed to get public IP"
exit 1
fi
log_message "Current public IP: $current_ip"
log_message ""
# Process each domain
success_count=0
fail_count=0
for domain_entry in "${DOMAINS[@]}"; do
if process_domain "$domain_entry" "$current_ip"; then
success_count=$((success_count + 1))
else
fail_count=$((fail_count + 1))
fi
done
log_message "=========================================="
log_message "Summary:"
log_message " Successful: $success_count"
log_message " Failed: $fail_count"
log_message " Total: ${#DOMAINS[@]}"
log_message "=========================================="
if [[ $fail_count -gt 0 ]]; then
exit 1
fi
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment