From dbf077e851e289a97ec9d1a45d5caa8bb755e132 Mon Sep 17 00:00:00 2001 From: carlitosbond Date: Tue, 28 Apr 2026 12:26:26 +0000 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20=E2=80=94=20porkbun=20DDNS?= =?UTF-8?q?=20for=20carloselugo.com?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Credentials sourced from .ddns-env (gitignored, chmod 600). --- .gitignore | 1 + porkbun-ddns-setup.md | 74 +++++++++++++++++++++++ porkbun-ddns-test.sh | 99 +++++++++++++++++++++++++++++++ porkbun-ddns.sh | 132 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 306 insertions(+) create mode 100644 .gitignore create mode 100644 porkbun-ddns-setup.md create mode 100755 porkbun-ddns-test.sh create mode 100755 porkbun-ddns.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32af123 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.ddns-env diff --git a/porkbun-ddns-setup.md b/porkbun-ddns-setup.md new file mode 100644 index 0000000..25342c4 --- /dev/null +++ b/porkbun-ddns-setup.md @@ -0,0 +1,74 @@ +# porkbun-ddns — DDNS automático para carloselugo.com + +Actualiza registros DNS tipo A en Porkbun cuando cambia la IP pública, y envía una notificación via ntfy. + +## Archivos + +| Archivo | Ruta | +|---|---| +| Script principal | `/opt/ddns/ddns_porkbun.sh` | +| Credenciales | `/opt/ddns/.ddns-env` | +| State file | `/home/netintel/.porkbun-ddns.last-ip` | +| Log | `/home/netintel/.porkbun-ddns.log` | + +## Credenciales + +Las credenciales sensibles están separadas del script en `/opt/ddns/.ddns-env` con permisos `600`: + +```bash +# /opt/ddns/.ddns-env +API_KEY="..." +SECRET_KEY="..." +NTFY_PASS="..." +``` + +El script hace `source /opt/ddns/.ddns-env` al inicio para cargarlas. + +> Si el proyecto se versiona con git, agregar `.ddns-env` al `.gitignore`. + +## Configuración del script + +Estas variables se configuran directamente en `ddns_porkbun.sh`: + +| Variable | Descripción | +|---|---| +| `DOMAIN` | Dominio principal | +| `SUBDOMAINS` | Array de subdominios a actualizar (`"@"` = root) | +| `STATE_FILE` | Ruta donde se guarda la última IP conocida | +| `LOG_FILE` | Ruta del archivo de log | +| `NTFY_URL` | URL del canal ntfy | +| `NTFY_USER` | Usuario ntfy | +| `DRYRUN` | `1` = simula updates y envía notificación real sin tocar DNS ni state | + +## Instalación + +```bash +# Dar permisos de ejecución +chmod +x /opt/ddns/ddns_porkbun.sh + +# Verificar permisos del env +chmod 600 /opt/ddns/.ddns-env +``` + +## Cron (cada 5 minutos) + +```bash +crontab -e +``` + +``` +*/5 * * * * /opt/ddns/ddns_porkbun.sh +``` + +## Verificar funcionamiento + +```bash +# Ejecución normal +bash /opt/ddns/ddns_porkbun.sh + +# Dry run — simula todos los updates y envía notificación real sin modificar DNS ni state +DRYRUN=1 bash /opt/ddns/ddns_porkbun.sh + +# Ver log en tiempo real +tail -f /home/netintel/.porkbun-ddns.log +``` diff --git a/porkbun-ddns-test.sh b/porkbun-ddns-test.sh new file mode 100755 index 0000000..3692b7e --- /dev/null +++ b/porkbun-ddns-test.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────── +# porkbun-ddns-test.sh +# Version de prueba — solo actualiza git.carloselugo.com +# ───────────────────────────────────────────────────────────── + +source /opt/ddns/.ddns-env + +DOMAIN="carloselugo.com" +SUBDOMAINS=("git") + +LOG_FILE="/home/netintel/porkbun-ddns-test.log" +API_BASE="https://api.porkbun.com/api/json/v3" + +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 response + response=$(curl -4 -sf --max-time 10 \ + -X POST "$API_BASE/dns/retrieveByNameType/$DOMAIN/A/$subdomain" \ + -H "Content-Type: application/json" \ + -d "{\"apikey\":\"$API_KEY\",\"secretapikey\":\"$SECRET_KEY\"}") + log " [API retrieve] $response" + echo "$response" | grep -o '"content":"[^"]*"' | head -1 | cut -d'"' -f4 +} + +update_record() { + local subdomain="$1" + local new_ip="$2" + # editByNameType — no record ID needed, no risk of accidental create + local result + result=$(curl -4 -sf --max-time 10 \ + -X POST "$API_BASE/dns/editByNameType/$DOMAIN/A/$subdomain" \ + -H "Content-Type: application/json" \ + -d "{\"apikey\":\"$API_KEY\",\"secretapikey\":\"$SECRET_KEY\",\"content\":\"$new_ip\",\"ttl\":\"600\"}") + log " [API editByNameType] $result" + echo "$result" | grep -o '"status":"[^"]*"' | head -1 | cut -d'"' -f4 +} + +main() { + log "=== PRUEBA INICIADA ===" + + local forced_ip="" + while getopts "f:" opt; do + case $opt in + f) forced_ip="$OPTARG" ;; + *) echo "Uso: $0 [-f ]"; exit 1 ;; + esac + done + + local current_ip + if [ -n "$forced_ip" ]; then + current_ip="$forced_ip" + log "IP forzada manualmente: $current_ip" + else + current_ip=$(get_public_ip) + if [ -z "$current_ip" ]; then + log "ERROR: No se pudo obtener la IP pública." + exit 1 + fi + log "IP pública actual: $current_ip" + fi + + for subdomain in "${SUBDOMAINS[@]}"; do + log "── Procesando: $subdomain ──" + + local dns_ip + dns_ip=$(get_dns_ip "$subdomain") + log " IP en DNS: ${dns_ip:-'(no encontrada)'}" + + if [ "$current_ip" = "$dns_ip" ]; then + log " [$subdomain] Ya está actualizado ($current_ip). No se hace nada." + continue + fi + + log " [$subdomain] Actualizando: ${dns_ip:-'?'} → $current_ip" + local status + status=$(update_record "$subdomain" "$current_ip") + + if [ "$status" = "SUCCESS" ]; then + log " [$subdomain] ✓ Actualizado correctamente." + else + log " [$subdomain] ✗ Falló. Status: $status" + fi + done + + log "=== PRUEBA TERMINADA ===" +} + +main "$@" \ No newline at end of file diff --git a/porkbun-ddns.sh b/porkbun-ddns.sh new file mode 100755 index 0000000..d0673ab --- /dev/null +++ b/porkbun-ddns.sh @@ -0,0 +1,132 @@ +#!/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 \ No newline at end of file