Created
December 28, 2025 00:10
-
-
Save Angus-Moore-Dev/88129a020b1e627ad90c0b7be0224cf1 to your computer and use it in GitHub Desktop.
Cloudflare DDNS Updater
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 | |
| # 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