Configuración de DNS Dinámico (DDNS)
El DNS dinámico (DDNS) permite mantener un nombre de dominio apuntando correctamente a un servidor cuya dirección IP cambia periódicamente, como ocurre en conexiones residenciales con IP dinámica de ISP o instancias en la nube con IPs flotantes. La solución puede implementarse con actualizaciones automáticas mediante la API de Cloudflare, nsupdate con BIND, o el cliente ddclient. Esta guía cubre todos estos métodos con scripts de actualización automática.
Requisitos Previos
- Servidor Linux (Ubuntu 22.04/Debian 12 o CentOS 9/Rocky 9)
- Nombre de dominio con acceso a la gestión DNS
- Para Cloudflare: cuenta gratuita y API token con permisos de DNS
- Para nsupdate: servidor BIND con actualizaciones dinámicas habilitadas
- Conexión a Internet para detectar la IP pública
DDNS con API de Cloudflare
El método más sencillo y fiable para DDNS usando Cloudflare como registrar DNS:
# Crear el script de actualización para Cloudflare
mkdir -p /opt/ddns
cat > /opt/ddns/cloudflare-ddns.sh << 'SCRIPT'
#!/bin/bash
# Script de actualización DDNS con la API de Cloudflare
# Configuración - ajustar con tus datos
CF_API_TOKEN="tu_token_api_cloudflare_aqui"
CF_ZONE_NAME="tudominio.com"
CF_RECORD_NAME="home.tudominio.com" # Registro A a actualizar
TTL=120 # TTL en segundos (mínimo 120 en Cloudflare)
PROXIED=false # true para pasar por el proxy de Cloudflare
# Archivo de estado para detectar cambios de IP
STATE_FILE="/var/cache/ddns/last-ip.txt"
LOG_FILE="/var/log/ddns/cloudflare-ddns.log"
mkdir -p /var/cache/ddns /var/log/ddns
# Función de logging con timestamp
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# Obtener la IP pública actual
get_public_ip() {
# Intentar varios servicios de detección de IP (por resiliencia)
for url in "https://ipv4.icanhazip.com" "https://api.ipify.org" "https://checkip.amazonaws.com"; do
IP=$(curl -s --max-time 5 "$url" | tr -d '[:space:]')
if [[ "$IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "$IP"
return 0
fi
done
return 1
}
# Obtener el ID de la zona de Cloudflare
get_zone_id() {
curl -s "https://api.cloudflare.com/client/v4/zones?name=${CF_ZONE_NAME}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" | \
python3 -c "import sys,json; data=json.load(sys.stdin); print(data['result'][0]['id'])"
}
# Obtener el ID del registro DNS específico
get_record_id() {
local zone_id="$1"
curl -s "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?type=A&name=${CF_RECORD_NAME}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" | \
python3 -c "import sys,json; data=json.load(sys.stdin); print(data['result'][0]['id'] if data['result'] else '')"
}
# Actualizar el registro DNS en Cloudflare
update_record() {
local zone_id="$1"
local record_id="$2"
local new_ip="$3"
if [ -z "$record_id" ]; then
# Crear el registro si no existe
RESPONSE=$(curl -s -X POST \
"https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"type\":\"A\",\"name\":\"${CF_RECORD_NAME}\",\"content\":\"${new_ip}\",\"ttl\":${TTL},\"proxied\":${PROXIED}}")
else
# Actualizar el registro existente
RESPONSE=$(curl -s -X PUT \
"https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"type\":\"A\",\"name\":\"${CF_RECORD_NAME}\",\"content\":\"${new_ip}\",\"ttl\":${TTL},\"proxied\":${PROXIED}}")
fi
echo "$RESPONSE" | python3 -c "import sys,json; data=json.load(sys.stdin); print('OK' if data['success'] else 'ERROR: ' + str(data['errors']))"
}
# PROGRAMA PRINCIPAL
CURRENT_IP=$(get_public_ip)
if [ $? -ne 0 ] || [ -z "$CURRENT_IP" ]; then
log "ERROR: No se pudo obtener la IP pública"
exit 1
fi
# Comparar con la última IP conocida
LAST_IP=$(cat "$STATE_FILE" 2>/dev/null)
if [ "$CURRENT_IP" = "$LAST_IP" ]; then
# IP sin cambios, no hacer nada
exit 0
fi
log "Cambio de IP detectado: $LAST_IP -> $CURRENT_IP"
# Actualizar en Cloudflare
ZONE_ID=$(get_zone_id)
RECORD_ID=$(get_record_id "$ZONE_ID")
RESULT=$(update_record "$ZONE_ID" "$RECORD_ID" "$CURRENT_IP")
if [ "$RESULT" = "OK" ]; then
echo "$CURRENT_IP" > "$STATE_FILE"
log "Registro DNS actualizado correctamente: ${CF_RECORD_NAME} → ${CURRENT_IP}"
else
log "ERROR actualizando DNS: $RESULT"
exit 1
fi
SCRIPT
chmod +x /opt/ddns/cloudflare-ddns.sh
DDNS con nsupdate y BIND
Para servidores BIND propios, nsupdate permite actualizar zonas de forma dinámica:
# Primero, configurar BIND para aceptar actualizaciones dinámicas
# En la definición de la zona en BIND (named.conf):
# zone "tudominio.com" {
# type master;
# file "/var/lib/bind/db.tudominio.com";
# allow-update { key ddns-key; }; // Solo actualizar con clave TSIG
# };
# Generar una clave TSIG para autenticar las actualizaciones
tsig-keygen -a HMAC-SHA256 ddns-key > /etc/bind/ddns-key.conf
# Ver la clave generada
cat /etc/bind/ddns-key.conf
# key "ddns-key" {
# algorithm hmac-sha256;
# secret "base64==";
# };
# Incluir la clave en la configuración de BIND
echo 'include "/etc/bind/ddns-key.conf";' >> /etc/bind/named.conf
# Recargar BIND
rndc reload
# Script de actualización con nsupdate
cat > /opt/ddns/nsupdate-ddns.sh << 'SCRIPT'
#!/bin/bash
DNS_SERVER="ns1.tudominio.com" # Servidor DNS a actualizar
ZONE="tudominio.com"
HOSTNAME="servidor.tudominio.com"
TTL=120
KEY_FILE="/etc/bind/ddns-key.conf"
# Obtener IP pública actual
NEW_IP=$(curl -s https://ipv4.icanhazip.com | tr -d '[:space:]')
if [ -z "$NEW_IP" ]; then
echo "ERROR: No se pudo obtener la IP pública"
exit 1
fi
# Actualizar el registro A con nsupdate
nsupdate -k "$KEY_FILE" << EOF
server $DNS_SERVER
zone $ZONE
update delete $HOSTNAME A
update add $HOSTNAME $TTL A $NEW_IP
send
EOF
if [ $? -eq 0 ]; then
echo "DNS actualizado: $HOSTNAME -> $NEW_IP"
else
echo "ERROR: nsupdate falló"
exit 1
fi
SCRIPT
chmod +x /opt/ddns/nsupdate-ddns.sh
DDNS con ddclient
ddclient es un cliente DDNS multi-protocolo que soporta Cloudflare, DynDNS, No-IP y muchos otros:
# Instalar ddclient
apt-get install -y ddclient # Ubuntu/Debian
# dnf install -y ddclient # CentOS/Rocky
# Configuración de ddclient para Cloudflare
cat > /etc/ddclient.conf << 'EOF'
# Configuración global
daemon=300 # Comprobar cambios cada 300 segundos
syslog=yes # Enviar logs al syslog
pid=/run/ddclient.pid
ssl=yes # Usar SSL para todas las comunicaciones
# Proveedor Cloudflare
protocol=cloudflare
use=web # Obtener IP desde servicio web externo
web=ipv4.icanhazip.com
zone=tudominio.com # Nombre de la zona en Cloudflare
[email protected] # Email de la cuenta Cloudflare (o "token")
password=tu_api_token_cloudflare
ttl=120
home.tudominio.com # Registro(s) A a actualizar
servidor.tudominio.com
EOF
# Permisos seguros para el archivo de configuración (contiene contraseñas)
chmod 600 /etc/ddclient.conf
# Habilitar e iniciar ddclient
systemctl enable --now ddclient
# Probar la configuración
ddclient -debug -verbose -noquiet
Detección Automática de IP Pública
# Método 1: Servicios externos (más fiable para IPs públicas)
curl -s https://ipv4.icanhazip.com
curl -s https://api.ipify.org
curl -s https://checkip.amazonaws.com
curl -s https://ipecho.net/plain
# Método 2: Desde la interfaz de red (para IPs directas sin NAT)
ip addr show eth0 | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1
# Método 3: Via DNS (preguntar a opendns cuál es tu IP)
dig +short myip.opendns.com @resolver1.opendns.com
# Script robusto de detección de IP con fallbacks
cat > /opt/ddns/get-public-ip.sh << 'SCRIPT'
#!/bin/bash
# Intentar múltiples fuentes para obtener la IP pública
SERVICES="https://ipv4.icanhazip.com https://api.ipify.org https://checkip.amazonaws.com"
for URL in $SERVICES; do
IP=$(curl -s --max-time 5 --retry 2 "$URL" | tr -d '[:space:]')
if [[ "$IP" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo "$IP"
exit 0
fi
done
echo "ERROR: No se pudo determinar la IP pública" >&2
exit 1
SCRIPT
chmod +x /opt/ddns/get-public-ip.sh
Script de Actualización Personalizado
# Script completo y robusto para múltiples registros
cat > /opt/ddns/multi-record-ddns.sh << 'SCRIPT'
#!/bin/bash
CF_API_TOKEN="${CF_API_TOKEN:?Variable CF_API_TOKEN no definida}"
LOG="/var/log/ddns/multi-ddns.log"
mkdir -p /var/log/ddns
log() { echo "[$(date '+%Y-%m-%dT%H:%M:%S')] $*" | tee -a "$LOG"; }
# Lista de registros a actualizar: "zona|nombre"
RECORDS=(
"tudominio.com|home.tudominio.com"
"tudominio.com|vpn.tudominio.com"
"otro-dominio.com|server.otro-dominio.com"
)
# Obtener IP actual
CURRENT_IP=$(/opt/ddns/get-public-ip.sh)
[ $? -ne 0 ] && { log "ERROR: No se pudo obtener IP"; exit 1; }
for RECORD in "${RECORDS[@]}"; do
ZONE="${RECORD%%|*}"
NAME="${RECORD##*|}"
# Obtener ID de zona
ZONE_ID=$(curl -s "https://api.cloudflare.com/client/v4/zones?name=${ZONE}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(d['result'][0]['id'])" 2>/dev/null)
# Obtener ID de registro
RECORD_DATA=$(curl -s "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?type=A&name=${NAME}" \
-H "Authorization: Bearer ${CF_API_TOKEN}")
RECORD_ID=$(echo "$RECORD_DATA" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',[]); print(r[0]['id'] if r else '')" 2>/dev/null)
CURRENT_DNS=$(echo "$RECORD_DATA" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',[]); print(r[0]['content'] if r else '')" 2>/dev/null)
if [ "$CURRENT_IP" = "$CURRENT_DNS" ]; then
log "Sin cambios: $NAME ($CURRENT_IP)"
continue
fi
# Actualizar registro
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"type\":\"A\",\"name\":\"${NAME}\",\"content\":\"${CURRENT_IP}\",\"ttl\":120}" | \
python3 -c "import sys,json; d=json.load(sys.stdin); print('OK: actualizado' if d['success'] else 'ERROR: '+str(d['errors']))" | \
while read LINE; do log "$NAME: $LINE"; done
done
SCRIPT
chmod +x /opt/ddns/multi-record-ddns.sh
Automatización con systemd Timer
# Crear el servicio systemd
cat > /etc/systemd/system/ddns-update.service << 'EOF'
[Unit]
Description=Actualización DNS dinámico
After=network-online.target
[Service]
Type=oneshot
ExecStart=/opt/ddns/cloudflare-ddns.sh
User=nobody
Group=nogroup
EnvironmentFile=-/etc/ddns/ddns.env
StandardOutput=journal
StandardError=journal
EOF
# Crear el timer (se ejecuta cada 5 minutos)
cat > /etc/systemd/system/ddns-update.timer << 'EOF'
[Unit]
Description=Timer de actualización DNS dinámico
Requires=network-online.target
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
AccuracySec=30s
[Install]
WantedBy=timers.target
EOF
# Activar el timer
systemctl daemon-reload
systemctl enable --now ddns-update.timer
# Verificar el timer
systemctl list-timers ddns-update.timer
Solución de Problemas
# Ver el log de actualizaciones DDNS
tail -f /var/log/ddns/cloudflare-ddns.log
# Ejecutar el script manualmente para probar
/opt/ddns/cloudflare-ddns.sh
# Verificar que el DNS se ha actualizado
dig +short home.tudominio.com @1.1.1.1 # Consultar a Cloudflare directamente
dig +short home.tudominio.com @8.8.8.8 # Verificar en Google DNS
# Comprobar el token de API de Cloudflare
curl -s "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer TU_TOKEN" | python3 -m json.tool
# Ver los últimos cambios del timer
journalctl -u ddns-update.service -n 20 --no-pager
systemctl status ddns-update.service
Conclusión
El DNS dinámico es una solución indispensable para mantener accesibles los servidores con IP variable, ya sea mediante la API de Cloudflare para dominios en ese registrar, nsupdate para servidores BIND propios, o ddclient para compatibilidad con múltiples proveedores. La automatización con systemd timers y la implementación de notificaciones ante cambios de IP garantizan que el DNS permanezca siempre actualizado sin intervención manual.


