Initial commit — porkbun DDNS for carloselugo.com
Credentials sourced from .ddns-env (gitignored, chmod 600).
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
.ddns-env
|
||||||
@@ -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
|
||||||
|
```
|
||||||
Executable
+99
@@ -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 <ip>]"; 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 "$@"
|
||||||
Executable
+132
@@ -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
|
||||||
Reference in New Issue
Block a user