Files
ddns/porkbun-ddns.sh
carlitosbond dbf077e851 Initial commit — porkbun DDNS for carloselugo.com
Credentials sourced from .ddns-env (gitignored, chmod 600).
2026-04-28 12:26:26 +00:00

132 lines
4.2 KiB
Bash
Executable File

#!/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