#!/usr/bin/env bash set -euo pipefail ### Function to log messages log() { echo "$(date "+%Y-%m-%d %H:%M:%S") $1" | tee -a "$LOG_FILE" >&2 } ### Function to log messages only to the log file log_to_file() { echo "$(date "+%Y-%m-%d %H:%M:%S") $1" >> "$LOG_FILE" } ### Create log file parent_path="$(dirname "${BASH_SOURCE[0]}")" LOG_FILE="${parent_path}/cloudflare-dns-update.log" touch "$LOG_FILE" log "==> Script started" ### Validate config file config_file="${1:-${parent_path}/cloudflare-dns-update.conf}" if ! source "$config_file"; then log "Error! Missing configuration file $config_file or invalid syntax!" exit 1 fi ### Check validity of parameters if ! [[ "$ttl" =~ ^[0-9]+$ ]] || { [ "$ttl" -lt 120 ] || [ "$ttl" -gt 7200 ]; } && [ "$ttl" -ne 1 ]; then log "Error! ttl must be 1 or between 120 and 7200" exit 1 fi ### Valid IPv4 and IPv6 Regex readonly IPV4_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$' readonly IPV6_REGEX='^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$' ### Function to get external IP (IPv4 or IPv6) get_external_ip() { local ip_type=$1 local sources=() local regex case "$ip_type" in ipv4) sources=("https://api.ipify.org" "https://checkip.amazonaws.com" "https://ifconfig.me/ip") regex="$IPV4_REGEX" ;; ipv6) sources=("https://api64.ipify.org" "https://ifconfig.co/ip") regex="$IPV6_REGEX" ;; *) log "Error! Invalid IP type specified: $ip_type" return 1 ;; esac for source in "${sources[@]}"; do if ip=$(curl -"${ip_type:3:1}" -s "$source" --max-time 10) && [[ "$ip" =~ $regex ]]; then echo "$ip" return 0 fi done log "Error! Unable to retrieve $ip_type address from any source." return 1 } ### Get external IPs ipv4=$(get_external_ip "ipv4") || ipv4="" ipv6=$(get_external_ip "ipv6") || ipv6="" [ -n "$ipv4" ] && log "==> External IPv4 is: $ipv4" [ -n "$ipv6" ] && log "==> External IPv6 is: $ipv6" ### Function to extract value from JSON json_extract() { local key=$1 sed -n 's/.*"'"$key"'":"\?\([^,"]*\)"\?.*/\1/p' } ### Function to update DNS record update_dns_record() { local record=$1 local ip=$2 local type=$3 local ttl=$4 local proxied=$5 # Get the DNS record information from Cloudflare API local cloudflare_record_info cloudflare_record_info=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zoneid/dns_records?type=$type&name=$record" \ -H "Authorization: Bearer $cloudflare_zone_api_token" \ -H "Content-Type: application/json") log_to_file "Cloudflare API response: $cloudflare_record_info" # Log the API response to file for debugging if [[ $cloudflare_record_info == *"\"success\":false"* ]]; then log "Error! Can't get $record ($type) record information from Cloudflare API" return 1 fi # Get the current IP and proxy status from the API response local current_ip current_ip=$(echo "$cloudflare_record_info" | json_extract "content") local current_proxied current_proxied=$(echo "$cloudflare_record_info" | json_extract "proxied") # Check if IP or proxy have changed if [ "$current_ip" == "$ip" ] && [ "$current_proxied" == "$proxied" ]; then log "==> DNS $type record of $record is $current_ip, no changes needed." return 0 fi log "==> DNS $type record of $record is: $current_ip. Trying to update..." # Get the DNS record ID from response local cloudflare_dns_record_id cloudflare_dns_record_id=$(echo "$cloudflare_record_info" | json_extract "id") # Push new DNS record information to Cloudflare API if ! curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zoneid/dns_records/$cloudflare_dns_record_id" \ -H "Authorization: Bearer $cloudflare_zone_api_token" \ -H "Content-Type: application/json" \ --data "{\"type\":\"$type\",\"name\":\"$record\",\"content\":\"$ip\",\"ttl\":$ttl,\"proxied\":$proxied}" | grep -q '"success":true'; then log "Error! Update failed for $record ($type)" return 1 fi log "==> Success!" log "==> $record DNS $type Record updated to: $ip, ttl: $ttl, proxied: $proxied" # Telegram notification if [ "${notify_me_telegram:-no}" == "yes" ]; then send_telegram_notification "$record" "$type" "$ip" "$ttl" "$proxied" fi } ### Function to send Telegram notification send_telegram_notification() { local record=$1 local type=$2 local ip=$3 local ttl=$4 local proxied=$5 if ! curl -s -X POST "https://api.telegram.org/bot${telegram_bot_API_Token}/sendMessage" \ -H "Content-Type: application/json" \ --data "{\"chat_id\":\"${telegram_chat_id}\",\"text\":\"DNS Record Update\nDomain: ${record}\nType: ${type}\nIP: ${ip}\nTTL: ${ttl}\nProxied: ${proxied}\"}" | grep -q '"ok":true'; then log "Error! Telegram notification failed for $record ($type)" fi } # Update DNS records for dns_record in "${dns_record[@]}"; do IFS=',' read -r record ttl proxied <<< "$dns_record" ttl="${ttl:-1}" # if TTL is null, set to automatic 1 proxied="${proxied:-true}" # if proxied is null, set true [ -n "$ipv4" ] && update_dns_record "$record" "$ipv4" "A" "$ttl" "$proxied" [ -n "$ipv6" ] && update_dns_record "$record" "$ipv6" "AAAA" "$ttl" "$proxied" done log "==> Script finished"