#!bin/bash #=================# # params #=================# TOKEN="cfut_wgrWeIbAuyqekbEd1EqCQFSR6NNmE0gzJdReSZoce3352365" AUTO_DELETE_ORPHANS=true # Set to true to auto-delete orphaned records DRY_RUN=false # Set to false to actually create records ################# # Domain list ################# declare -a domains # declare main domain associative array to include all domains in one array declare -A d0 d1 # declare new array for each domain d0=( ["domain"]="" ["zone_id"]="" ["subdomains"]="@,git" ["protected_records"]="" ) domains=( d0 d1 ) #*****************# # DNS FUNCTIONS #*****************# get_current_ip() { curl -s https://ifconfig.me/ip } list_dns_records() { local zone_id="$1" curl -s "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records" \ -H "Authorization: Bearer $TOKEN" } create_dns_record() { local zone_id="$1" local data="$2" if [[ "$DRY_RUN" == true ]]; then echo " [DRY RUN] Would create record with: $data" echo '{"success":true,"dry_run":true}' return 0 fi curl -s "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records" \ -X POST \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $TOKEN" \ -d "$data" } delete_dns_record() { local zone_id="$1" local dns_record_id="$2" if [[ "$DRY_RUN" == true ]]; then echo " [DRY RUN] Would delete record ID: $dns_record_id" echo '{"success":true,"dry_run":true}' return 0 fi curl -s "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$dns_record_id" \ -X DELETE \ -H "Authorization: Bearer $TOKEN" } update_dns_record() { local zone_id="$1" local dns_record_id="$2" local body="$3" if [[ "$DRY_RUN" == true ]]; then echo " [DRY RUN] Would update record ID: $dns_record_id with: $body" echo '{"success":true,"dry_run":true}' return 0 fi curl -s "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$dns_record_id" \ -X PATCH \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $TOKEN" \ -d "$body" } #*****************# # HELPER FUNCTIONS #*****************# compare_ips() { local ip1="$1" local ip2="$2" if [[ "$ip1" == "$ip2" ]]; then return 0 # IPs are the same else return 1 # IPs are different fi } #**************# # MAIN OPERATIONS #**************# # Display mode if [[ "$DRY_RUN" == true ]]; then echo "⚠️ DRY RUN MODE ENABLED - No actual changes will be made" echo "=========================================" fi # Get current public IP current_ip=$(get_current_ip) if [ -z "$current_ip" ]; then echo "Error: Could not retrieve current IP" exit 1 fi echo "Current Public IP: $current_ip" echo "=========================================" # Process each domain for item in "${domains[@]}"; do unset domain declare -n domain="$item" echo "Processing domain: ${domain[domain]}" echo "Zone ID: ${domain[zone_id]}" # Parse protected records IFS=',' read -ra protected_records <<< "${domain[protected_records]}" # Parse managed subdomains IFS=',' read -ra managed_subdomains <<< "${domain[subdomains]}" echo "Managed subdomains: ${managed_subdomains[@]}" # Get existing DNS records response=$(list_dns_records "${domain[zone_id]}") # Check for API errors if echo "$response" | jq -e '.success == false' > /dev/null 2>&1; then echo "Error: Failed to get DNS records" echo "$response" | jq '.errors' continue fi echo "----------------------------------------" # Create associative array for existing A records declare -A existing_a_records # Build map of existing A records echo "Existing A records:" while IFS= read -r record; do if [[ -n "$record" ]]; then record_id=$(echo "$record" | jq -r '.id') record_name=$(echo "$record" | jq -r '.name') record_content=$(echo "$record" | jq -r '.content') existing_a_records["$record_name"]="$record_id|$record_content" echo " - $record_name → $record_content (ID: $record_id)" fi done < <(echo "$response" | jq -c '.result[] | select(.type == "A")') echo "----------------------------------------" # Statistics counters records_created=0 records_updated=0 records_skipped=0 records_deleted=0 # Process each managed subdomain (create/update) for subdomain in "${managed_subdomains[@]}"; do # Build full domain name if [[ "$subdomain" == "@" ]] || [[ -z "$subdomain" ]]; then full_name="${domain[domain]}" else full_name="${subdomain}.${domain[domain]}" fi echo "Checking: $full_name" # Check if record exists if [[ -n "${existing_a_records[$full_name]}" ]]; then IFS='|' read -r record_id existing_ip <<< "${existing_a_records[$full_name]}" # Compare IPs if compare_ips "$existing_ip" "$current_ip"; then echo " ✓ SKIPPED: IP unchanged ($existing_ip = $current_ip)" ((records_skipped++)) else echo " ⚠ NEEDS UPDATE: IP changed ($existing_ip → $current_ip)" # Update existing record with new IP update_body=$(jq -n --arg ip "$current_ip" '{"content": $ip}') update_response=$(update_dns_record "${domain[zone_id]}" "$record_id" "$update_body") if echo "$update_response" | jq -e '.success == true' > /dev/null 2>&1; then echo " ✓ SUCCESS: Updated $full_name to $current_ip" ((records_updated++)) else echo " ✗ FAILED: Could not update $full_name" echo "$update_response" | jq '.errors' fi fi # Mark as processed (remove from map for deletion check) unset existing_a_records["$full_name"] else echo " ➕ CREATING: Record does not exist" # Create new record create_data=$(jq -n \ --arg name "$full_name" \ --arg ip "$current_ip" \ '{ "type": "A", "name": $name, "content": $ip, "ttl": 1, "proxied": true }') create_response=$(create_dns_record "${domain[zone_id]}" "$create_data") if echo "$create_response" | jq -e '.success == true' > /dev/null 2>&1; then echo " ✓ SUCCESS: Created $full_name -> $current_ip" ((records_created++)) else echo " ✗ FAILED: Could not create $full_name" echo "$create_response" | jq '.errors' fi fi echo "" done # Delete orphaned records if auto-delete is enabled if [[ "$AUTO_DELETE_ORPHANS" == true ]] && [[ ${#existing_a_records[@]} -gt 0 ]]; then echo "Checking for orphaned records to delete..." for record_name in "${!existing_a_records[@]}"; do # Check if record is protected is_protected=false for protected in "${protected_records[@]}"; do if [[ "$record_name" == "$protected" ]]; then is_protected=true break fi done if [[ "$is_protected" == true ]]; then echo " ⚠ PROTECTED: $record_name (skipped from deletion)" continue fi IFS='|' read -r record_id existing_ip <<< "${existing_a_records[$record_name]}" echo " 🗑 ORPHANED: $record_name -> $existing_ip" delete_response=$(delete_dns_record "${domain[zone_id]}" "$record_id") if echo "$delete_response" | jq -e '.success == true' > /dev/null 2>&1; then echo " ✓ DELETED: $record_name" ((records_deleted++)) else echo " ✗ FAILED: Could not delete $record_name" echo "$delete_response" | jq '.errors' fi done echo "" elif [[ ${#existing_a_records[@]} -gt 0 ]]; then echo "Orphaned records found (auto-delete disabled):" for record_name in "${!existing_a_records[@]}"; do IFS='|' read -r record_id existing_ip <<< "${existing_a_records[$record_name]}" echo " - $record_name -> $existing_ip (ID: $record_id)" done echo "" fi # Summary echo "=========================================" echo "SUMMARY for ${domain[domain]}:" echo " ✓ Created: $records_created" echo " ✓ Updated: $records_updated" echo " ✓ Skipped (IP unchanged): $records_skipped" echo " ✓ Deleted: $records_deleted" echo "=========================================" echo "" # Clean up associative array unset existing_a_records done if [[ "$DRY_RUN" == true ]]; then echo "⚠️ DRY RUN COMPLETED - No actual changes were made" echo "Set DRY_RUN=false to apply changes" fi