#!/usr/bin/env bash # ───────────────────────────────────────────────────────────── # porkbun-ddns.sh — DDNS para carloselugo.com # Detecta cambio de IP pública y actualiza registros A # en Porkbun via editByNameType (no requiere record ID) # # Instalar: # sudo cp porkbun-ddns.sh /opt/scripts/ # sudo chmod +x /opt/scripts/porkbun-ddns.sh # # Cron (cada 5 minutos): # sudo crontab -e # */5 * * * * /opt/scripts/porkbun-ddns.sh # # Log: /home/netintel/.porkbun-ddns.log # State: /home/netintel/.porkbun-ddns.last-ip # ───────────────────────────────────────────────────────────── source /opt/ddns/.ddns-env DOMAIN="carloselugo.com" SUBDOMAINS=("@" "gamesever1" "git") # "@" = root domain STATE_FILE="/home/netintel/.porkbun-ddns.last-ip" LOG_FILE="/home/netintel/.porkbun-ddns.log" API_BASE="https://api.porkbun.com/api/json/v3" NTFY_URL="https://ntfy.carloselugo.com/homelab" NTFY_USER="carlos" DRYRUN=0 # 1 = simula updates y envía notificación real sin tocar DNS ni state log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" >&2; } get_public_ip() { local ip ip=$(curl -4 -sf --max-time 5 https://api.ipify.org) || ip=$(curl -4 -sf --max-time 5 https://ifconfig.me) || ip=$(curl -4 -sf --max-time 5 https://icanhazip.com) echo "$ip" } get_dns_ip() { local subdomain="$1" local endpoint [ "$subdomain" = "@" ] && endpoint="$API_BASE/dns/retrieveByNameType/$DOMAIN/A" \ || endpoint="$API_BASE/dns/retrieveByNameType/$DOMAIN/A/$subdomain" local response response=$(curl -4 -sf --max-time 10 \ -X POST "$endpoint" \ -H "Content-Type: application/json" \ -d "{\"apikey\":\"$API_KEY\",\"secretapikey\":\"$SECRET_KEY\"}") echo "$response" | grep -o '"content":"[^"]*"' | head -1 | cut -d'"' -f4 } update_record() { local subdomain="$1" local new_ip="$2" local endpoint [ "$subdomain" = "@" ] && endpoint="$API_BASE/dns/editByNameType/$DOMAIN/A" \ || endpoint="$API_BASE/dns/editByNameType/$DOMAIN/A/$subdomain" local result result=$(curl -4 -sf --max-time 10 \ -X POST "$endpoint" \ -H "Content-Type: application/json" \ -d "{\"apikey\":\"$API_KEY\",\"secretapikey\":\"$SECRET_KEY\",\"content\":\"$new_ip\",\"ttl\":\"600\"}") echo "$result" | grep -o '"status":"[^"]*"' | head -1 | cut -d'"' -f4 } main() { local current_ip current_ip=$(get_public_ip) if [ -z "$current_ip" ]; then log "ERROR: No se pudo obtener la IP pública." exit 1 fi # Leer última IP conocida local last_ip="" [ -f "$STATE_FILE" ] && last_ip=$(cat "$STATE_FILE") # Si no cambió, salir silenciosamente (DRYRUN lo omite para forzar notificación) if [ "$current_ip" = "$last_ip" ] && [ "$DRYRUN" != "1" ]; then exit 0 fi log "IP cambió: ${last_ip:-'(primera ejecución)'} → $current_ip" local updated=0 for subdomain in "${SUBDOMAINS[@]}"; do local dns_ip dns_ip=$(get_dns_ip "$subdomain") if [ "$DRYRUN" = "1" ]; then log " [$subdomain] [DRY RUN] ${dns_ip:-'?'} → $current_ip (sin cambios reales)" ((updated++)) continue fi if [ "$current_ip" = "$dns_ip" ]; then log " [$subdomain] Ya actualizado ($current_ip)" continue fi local status status=$(update_record "$subdomain" "$current_ip") if [ "$status" = "SUCCESS" ]; then log " [$subdomain] ✓ $dns_ip → $current_ip" ((updated++)) else log " [$subdomain] ✗ Falló. Status: ${status:-'(sin respuesta)'}" fi done # Guardar nueva IP en state file (DRYRUN no escribe para no alterar el estado real) [ "$DRYRUN" != "1" ] && echo "$current_ip" > "$STATE_FILE" if [ $updated -gt 0 ]; then log "Listo. $updated registro(s) actualizado(s)." curl -4 -s \ -u "${NTFY_USER}:${NTFY_PASS}" \ -H "Title: 🌐 IP Pública Cambió" \ -H "Tags: warning" \ -H "Priority: default" \ -d "$(hostname): ${last_ip:-'primera vez'} → ${current_ip} ($updated registro(s) actualizado(s))" \ "$NTFY_URL" > /dev/null fi } main