#!/usr/bin/env bash # Update all Cloudflare DNS records to point to single public IP (76.53.10.36) # Sets all records to DNS only mode (gray cloud) for direct NAT routing # Supports multiple zones: sankofa.nexus, d-bis.org, mim4u.org, defi-oracle.io # UDM Pro port forwarding: 76.53.10.36:80/443 → ${IP_NPMPLUS:-${IP_NPMPLUS:-192.168.11.167}}:80/443 (NPMplus) set -euo pipefail # Load IP configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } log_success() { echo -e "${GREEN}[✓]${NC} $1"; } log_warn() { echo -e "${YELLOW}[⚠]${NC} $1"; } log_error() { echo -e "${RED}[✗]${NC} $1"; } # Script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Source .env file (set +u so values with $ in them don't trigger unbound variable) if [ -f "$PROJECT_ROOT/.env" ]; then set +u # shellcheck source=/dev/null source "$PROJECT_ROOT/.env" set -u else log_error ".env file not found at $PROJECT_ROOT/.env" exit 1 fi # Public IP for all services (76.53.10.36 - UDM Pro port forwarding to NPMplus ${IP_NPMPLUS:-${IP_NPMPLUS:-192.168.11.167}}) PUBLIC_IP="${PUBLIC_IP:-76.53.10.36}" # Cloudflare authentication if [ -n "${CLOUDFLARE_API_TOKEN:-}" ]; then AUTH_HEADER="Authorization: Bearer $CLOUDFLARE_API_TOKEN" log_info "Using API Token authentication" elif [ -n "${CLOUDFLARE_EMAIL:-}" ] && [ -n "${CLOUDFLARE_API_KEY:-}" ]; then AUTH_HEADER_EMAIL="$CLOUDFLARE_EMAIL" AUTH_HEADER_KEY="$CLOUDFLARE_API_KEY" log_info "Using Email/API Key authentication" else log_error "Missing Cloudflare credentials" log_error "Required: CLOUDFLARE_API_TOKEN OR (CLOUDFLARE_EMAIL + CLOUDFLARE_API_KEY)" exit 1 fi # Zone IDs (can be set in .env or passed as parameters) ZONE_SANKOFA_NEXUS="${CLOUDFLARE_ZONE_ID_SANKOFA_NEXUS:-}" ZONE_D_BIS_ORG="${CLOUDFLARE_ZONE_ID_D_BIS_ORG:-${CLOUDFLARE_ZONE_ID:-}}" ZONE_MIM4U_ORG="${CLOUDFLARE_ZONE_ID_MIM4U_ORG:-}" ZONE_DEFI_ORACLE_IO="${CLOUDFLARE_ZONE_ID_DEFI_ORACLE_IO:-}" # Function to make Cloudflare API request cf_api_request() { local method="$1" local zone_id="$2" local endpoint="$3" local data="${4:-}" local url="https://api.cloudflare.com/client/v4/zones/${zone_id}${endpoint}" if [ -n "${CLOUDFLARE_API_TOKEN:-}" ]; then if [ -n "$data" ]; then curl -s -X "$method" "$url" \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ -H "Content-Type: application/json" \ --data "$data" else curl -s -X "$method" "$url" \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ -H "Content-Type: application/json" fi else if [ -n "$data" ]; then curl -s -X "$method" "$url" \ -H "X-Auth-Email: $AUTH_HEADER_EMAIL" \ -H "X-Auth-Key: $AUTH_HEADER_KEY" \ -H "Content-Type: application/json" \ --data "$data" else curl -s -X "$method" "$url" \ -H "X-Auth-Email: $AUTH_HEADER_EMAIL" \ -H "X-Auth-Key: $AUTH_HEADER_KEY" \ -H "Content-Type: application/json" fi fi } # Function to get existing DNS record get_dns_record() { local zone_id="$1" local name="$2" local type="${3:-A}" local response=$(cf_api_request "GET" "$zone_id" "/dns_records?name=${name}&type=${type}") echo "$response" | jq -r '.result[0] // empty' 2>/dev/null || echo "" } # Function to delete DNS record delete_dns_record() { local zone_id="$1" local record_id="$2" local response=$(cf_api_request "DELETE" "$zone_id" "/dns_records/$record_id") if echo "$response" | jq -e '.success' >/dev/null 2>&1; then return 0 else return 1 fi } # Function to get all DNS records (any type) for a name get_all_dns_records() { local zone_id="$1" local name="$2" local response=$(cf_api_request "GET" "$zone_id" "/dns_records?name=${name}") echo "$response" | jq -r '.result[] // empty' 2>/dev/null || echo "" } # Function to create or update DNS A record create_or_update_dns_record() { local zone_id="$1" local zone_name="$2" local name="$3" local ip="$4" local proxied="${5:-false}" # Handle apex domain (@ symbol) and construct full domain name local full_name if [ "$name" = "@" ]; then full_name="${zone_name}" elif [[ "$name" == *".${zone_name}" ]] || [[ "$name" == "${zone_name}" ]]; then # Already a full domain name (ends with zone name or is zone name) full_name="${name}" else # Subdomain - append zone name full_name="${name}.${zone_name}" fi log_info "Processing: $full_name → $ip (proxied: $proxied)" # Check for existing CNAME records (must delete before creating A record) local all_records=$(get_all_dns_records "$zone_id" "$full_name") if [ -n "$all_records" ] && [ "$all_records" != "null" ]; then echo "$all_records" | jq -r 'select(.type == "CNAME") | .id' | while read -r cname_id; do if [ -n "$cname_id" ] && [ "$cname_id" != "null" ]; then log_info " Deleting existing CNAME record (ID: ${cname_id:0:8}...)" if delete_dns_record "$zone_id" "$cname_id"; then log_success " Deleted CNAME record" else log_warn " Failed to delete CNAME record" fi fi done fi # Get existing A record local existing=$(get_dns_record "$zone_id" "$full_name" "A") local data=$(jq -n \ --arg name "$full_name" \ --arg content "$ip" \ --argjson proxied "$proxied" \ '{ type: "A", name: $name, content: $content, proxied: $proxied, ttl: 1 }') if [ -n "$existing" ] && [ "$existing" != "null" ]; then local record_id=$(echo "$existing" | jq -r '.id') log_info " Updating existing record (ID: ${record_id:0:8}...)" local response=$(cf_api_request "PUT" "$zone_id" "/dns_records/$record_id" "$data") if echo "$response" | jq -e '.success' >/dev/null 2>&1; then log_success " Updated: $full_name" return 0 else local error=$(echo "$response" | jq -r '.errors[0].message // "Unknown error"' 2>/dev/null || echo "Unknown error") log_error " Failed to update: $error" return 1 fi else log_info " Creating new record" local response=$(cf_api_request "POST" "$zone_id" "/dns_records" "$data") if echo "$response" | jq -e '.success' >/dev/null 2>&1; then log_success " Created: $full_name" return 0 else local error=$(echo "$response" | jq -r '.errors[0].message // "Unknown error"' 2>/dev/null || echo "Unknown error") log_error " Failed to create: $error" return 1 fi fi } # Function to process a zone process_zone() { local zone_id="$1" local zone_name="$2" shift 2 local records=("$@") if [ -z "$zone_id" ]; then log_warn "Skipping zone $zone_name (no zone ID configured)" return 0 fi log_info "" log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" log_info "Processing Zone: $zone_name" log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" local success_count=0 local fail_count=0 for record in "${records[@]}"; do if create_or_update_dns_record "$zone_id" "$zone_name" "$record" "$PUBLIC_IP" "false"; then ((success_count++)) else ((fail_count++)) fi done log_info "" log_info "Zone Summary: $success_count succeeded, $fail_count failed" return $fail_count } # Main execution main() { echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "🔧 Cloudflare DNS Update - Direct Public IP Routing" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" log_info "Public IP: $PUBLIC_IP" log_info "Proxy Mode: DNS Only (gray cloud)" echo "" local total_failures=0 # sankofa.nexus domain records if [ -n "$ZONE_SANKOFA_NEXUS" ]; then SANKOFA_RECORDS=( "@" # sankofa.nexus "www" # www.sankofa.nexus "phoenix" # phoenix.sankofa.nexus "www.phoenix" # www.phoenix.sankofa.nexus "the-order" # the-order.sankofa.nexus ) if ! process_zone "$ZONE_SANKOFA_NEXUS" "sankofa.nexus" "${SANKOFA_RECORDS[@]}"; then ((total_failures++)) fi else log_warn "Skipping sankofa.nexus (no zone ID configured)" fi # d-bis.org domain records if [ -n "$ZONE_D_BIS_ORG" ]; then DBIS_RECORDS=( "rpc-http-pub" # rpc-http-pub.d-bis.org "rpc-ws-pub" # rpc-ws-pub.d-bis.org "rpc" # rpc.d-bis.org (primary RPC) "rpc2" # rpc2.d-bis.org (secondary RPC) "ws.rpc" # ws.rpc.d-bis.org (primary WebSocket) "ws.rpc2" # ws.rpc2.d-bis.org (secondary WebSocket) "rpc-http-prv" # rpc-http-prv.d-bis.org "rpc-ws-prv" # rpc-ws-prv.d-bis.org "explorer" # explorer.d-bis.org "dbis-admin" # dbis-admin.d-bis.org "dbis-api" # dbis-api.d-bis.org "dbis-api-2" # dbis-api-2.d-bis.org "secure" # secure.d-bis.org ) if ! process_zone "$ZONE_D_BIS_ORG" "d-bis.org" "${DBIS_RECORDS[@]}"; then ((total_failures++)) fi else log_warn "Skipping d-bis.org (no zone ID configured)" fi # mim4u.org domain records if [ -n "$ZONE_MIM4U_ORG" ]; then MIM4U_RECORDS=( "@" # mim4u.org "www" # www.mim4u.org "secure" # secure.mim4u.org "training" # training.mim4u.org ) if ! process_zone "$ZONE_MIM4U_ORG" "mim4u.org" "${MIM4U_RECORDS[@]}"; then ((total_failures++)) fi else log_warn "Skipping mim4u.org (no zone ID configured)" fi # defi-oracle.io domain records if [ -n "$ZONE_DEFI_ORACLE_IO" ]; then DEFI_ORACLE_RECORDS=( "explorer" # explorer.defi-oracle.io (Blockscout - same as explorer.d-bis.org) "rpc.public-0138" # rpc.public-0138.defi-oracle.io "rpc" # rpc.defi-oracle.io (HTTP RPC) "wss" # wss.defi-oracle.io (WebSocket RPC) ) if ! process_zone "$ZONE_DEFI_ORACLE_IO" "defi-oracle.io" "${DEFI_ORACLE_RECORDS[@]}"; then ((total_failures++)) fi else log_warn "Skipping defi-oracle.io (no zone ID configured)" fi # Summary echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [ $total_failures -eq 0 ]; then log_success "✅ DNS Update Complete" else log_warn "⚠️ DNS Update Complete with $total_failures zone(s) having failures" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" log_info "📋 Summary:" log_info " • All records point to: $PUBLIC_IP" log_info " • Proxy mode: DNS Only (gray cloud)" log_info " • Routing: Direct NAT → Nginx → Backend Services" echo "" log_info "⏳ Wait 1-5 minutes for DNS propagation" log_info "🧪 Test with: dig sankofa.nexus +short" log_info "🧪 Test with: dig secure.d-bis.org +short" echo "" return $total_failures } # Run main function main "$@"