Cloudflare DDNS

Anteriormente intenté escribir una entrada usando Namecheap, su servicio DDNS y algunas herramientas. Todos los scripts funcionan perfecto excepto porque en tres días explorando opciones no había solucionado el problema de inicializar el pod desde systemd. Por este motivo decidí cambiar, probar otras opciones. La siguiente idea en complejidad que se me ocurrió fue utilizar Cloudflare y su servicio de DDNS.

Hay algunos usuarios que lo han hecho funcionar utilizando la UniFi Dream Machine, pero antes de adaptarlo a mi capado USG, prefiero ejecutarlo en mi servidor bastión, hobbiton.

Creando la zona en Cloudflare

Ingresamos al panel de control de Cloudflare y creamos una nueva zona escribiendo el dominio que vamos a usar en el asistente. Esto llevara a una pantalla de selección de los registros que queremos importar y al final nos mostrará unos servidores de nombre que debemos actualizar en Namecheap.

En mi caso me tocaron henrick y zita…

Ajustes en Namecheap

Ahora hay que ajustar el o los dominios que vamos a utilizar en Namecheap para que apunten a los servidores de nombre de Cloudflare del paso anterior.

Ajustando Namecheap

Hechos estos cambios hay que esperar un rato a que ocurra la propagación de los servidores de nombre. El caso es que Cloudflare te avisa vía correo electrónico cuando esto ocurre. Recibí la notificación aproximadamente 90 minutos después de hacer los ajustes.

Configurando Cloudflare

Como Cloudflare se trae los registros previos, Al entrar a la zona veremos los registros cargados.

Registros previos cargados

Ahora necesitamos una clave API y los tokens para poder gestionar remotamente los cambios. Nos vamos al panel de tokens de API de Cloudflare. Creamos una nuevo token usando la plantilla: Editar zona DNS. Aquí podemos delimitar el alcance del token pudiendo seleccionar zonas específicas y restringiendo o permitiendo IPs de acceso.

Configuración de Tokens de API

Una vez elegidas las opciones te mostrará una pantalla de resumen y luego el Token que no se volverá a mostrar nuevamente, por lo que conviene tenerlo bien resguardado. También se mostraráeun comando curl para verificar, similar a este:

1
2
3
curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
     -H "Authorization: Bearer JerHaBiloRenXN7DlJF_KX4B8u9f69HutlXRj1d3" \
     -H "Content-Type:application/json"

Ahora toca volver nuevamente al panel de Tokens de API y copiar o crear una nueva Global API Key y en nuestra zona para el dominio que hemos incorporado, copiamos Id de la zona y Id de la cuenta.

Hasta este momento debemos tener cuatro claves alfanuméricas de cada uno de estos valores y son las que utilizaremos en el contenedor de podman más adelante.

Intentando con podman

En mi bastión he preparado un directorio dentro de mi /home para usar con el contenedor a partir de una imagen que prepara timothymiller. Dentro hay un archivo llamado config.json cuyo contenido es el siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
    "cloudflare": [{
        "authentication": {
            "api_token": "JerHaBiloRenXN7DlJF_KX4B8u9f69HutlXRj1d3",
            "api_key": {
                "api_key": "1024a46d930e0f9bd89234c48dcd7ae54c7ee",
                "account_email": "demo@ejemplo.com"
            }
        },
        "zone_id": "5a7326f674c3a325beaf404b4e5623a0",
        "subdomains": [{
                "name": "",
                "proxied": false
            },
            {
                "name": "www",
                "proxied": false
            },
            {
                "name": "cloud",
                "proxied": false
            }
        ]
    }],
    "a": true,
    "aaaa": false,
    "purgeUnknownRecords": false,
    "ttl": 300
}

y otro llamado ddns_cloudflare.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
CONTAINER=ddns-cloudflare

if podman container exists "$CONTAINER"; then
   podman start "$CONTAINER"
else
   podman run -i -d --rm \
      --net=host \
      --name "$CONTAINER" \
      --security-opt=no-new-privileges \
      -v /home/linkmoises/ddns-cloudflare/config.json:/config.json \
      timothyjmiller/cloudflare-ddns:latest
fi

Y le damos permiso de ejecución chmod +x ddns-cloudflare.sh. Una vez ejecutado, veremos en los logs que se actualizan los valores.

1
2
3
4
5
6
7
🕰️ Updating IPv4 (A) records every 300 seconds

📡 Updating record {'type': 'A', 'name': 'serrano.red', 'content': '201.227.63.233', 'proxied': False, 'ttl': 300}

📡 Updating record {'type': 'A', 'name': 'www.serrano.red', 'content': '201.227.63.233', 'proxied': False, 'ttl': 300}

📡 Updating record {'type': 'A', 'name': 'cloud.serrano.red', 'content': '201.227.63.233', 'proxied': False, 'ttl': 300}

Al final, si ejecuto el pod funciona sin problemas, pero nuevamente al configurar la unidad en systemd para que el supervisor la mantenga funcionando falla. Por lo que opté por abandonar este método y volver a la vieja confiable, scripts de bash y cron.

La vieja confiable cron + scripts bash

Después de tantos problemas con podman (empiezo a pensar que me odia…) decidí buscar una alternativa basada en un script bash que se ejecute por medio de cron, así encontré este gist de Tras2.

Creamos un directorio en el /home/usuario donde trabajaremos esto: ddns-cloudflare, dentro he creado un archivo llamado ddns-registros.txt donde mantendré los registros por línea según el siguiente formato zona,dominio de manera que podría actualizar diferentes dominios y zonas simultáneamente. Cada entrada sin espacios y valores separados por coma y verificar que no hayan espacios al final del archivo.

1
2
3
serrano.red,serrano.red
serrano.red,www.serrano.red
serrano.red,cloud.serrano.red

Y añadimos un archivo ddns-cloudflare.sh donde volcaremos el script de abajo. Recordar dar permisos de ejecución chmod +x ddns-cloudflare.sh. El script en cuestión solo requiere el Token API de Cloudflare y el correo electrónico asociado. Este es el script bash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#!/bin/bash

################################################################################
##                                                                            ##
##  Basado en https://gist.github.com/Tras2/cba88201b17d765ec065ccbedfb16d9a  ##
##  este script comprueba y actualiza cambios en la dirección IP que ofrece   ##
##  nuestro proveedor, de manera que a través de la API de Cloudflare se      ##
##  actualizan los registros A y AAAA cuando estan disponibles.               ##
##                                                                            ##
##  Este script tiene como requisito la presencia de curl y jq instalados     ##
##  en el sistema para poder funcionar correctamente.                         ##
##                                                                            ##
################################################################################


## Datos iniciales a ser llenados por el usuario
# API token; e.g. JerHaBiloRenXN7DlJF-KX4B8u9f69HutlXRj1d3
api_token=jJerHaBiloRenXN7DlJF-KX4B8u9f69HutlXRj1d3
# Dirección de email asociada a la cuenta de Cloudflare; e.g. email@gmail.com
email=email@correo.com


# Datos básicos IPv4, IPv6 y usuario
ipv4=$(curl -4 -s -X GET https://checkip.amazonaws.com --max-time 10)
ipv6=$(curl -s -X GET -6 https://ifconfig.co)
user_id=$(curl -s -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
    -H "Authorization: Bearer $api_token" \
    -H "Content-Type:application/json" \
    | jq -r '{"result"}[] | .id'
)


# Anuncia datos de IPv4 e IPv6
if [ $ipv4 ]; then echo -e "\033[0;32m [+] Tú dirección IPv4 es: $ipv4"; else echo -e "\033[0;33m [!] No se puede obtener una dirección IPv4."; fi
if [ $ipv6 ]; then echo -e "\033[0;32m [+] Tu dirección IPv6 es: $ipv6"; else echo -e "\033[0;33m [!] No se puede obtener una dirección IPv6."; fi


# Lee el archivo de registros y zonas ddns-registros.txt
while IFS=, read -r zone_name dns_record; do

# verifica si el usuario de la API y el email son correctos
if [ $user_id ]
then
    zone_id=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$zone_name&status=active" \
                   -H "Content-Type: application/json" \
                   -H "X-Auth-Email: $email" \
                   -H "Authorization: Bearer $api_token" \
              | jq -r '{"result"}[] | .[0] | .id'
             )
    # verifica si en ID de la zona está disponible
    if [ $zone_id ]
    then
        # verifica si hay una versión IPv4
        if [ $ipv4 ]
        then
            dns_record_a_id=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records?type=A&name=$dns_record"  \
                                   -H "Content-Type: application/json" \
                                   -H "X-Auth-Email: $email" \
                                   -H "Authorization: Bearer $api_token"
                             )
            # si el registro IPv4 existe
            dns_record_a_ip=$(echo $dns_record_a_id |  jq -r '{"result"}[] | .[0] | .content')
            if [ $dns_record_a_ip != $ipv4 ]
            then
                # cambia el registro A
                curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$(echo $dns_record_a_id | jq -r '{"result"}[] | .[0] | .id')" \
                     -H "Content-Type: application/json" \
                     -H "X-Auth-Email: $email" \
                     -H "Authorization: Bearer $api_token" \
                     --data "{"type":"A","name":"$dns_record","content":"$ipv4","ttl":300,"proxied":false}" \
                | jq -r '.errors'
                # escribe el resultado
                echo -e "\033[0;32m [+] El registro A de $dns_record ha sido actualizado: $dns_record_a_ip -> $ipv4"
            else
                echo -e "\033[0;37m [~] El registro A de $dns_record es el mismo que la IPv4 actual."
            fi
        fi

        # verifica si hay una versión IPv6
        if [ $ipv6 ]
        then
            dns_record_aaaa_id=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records?type=AAAA&name=$dns_record"  \
                                      -H "Content-Type: application/json" \
                                      -H "X-Auth-Email: $email" \
                                      -H "Authorization: Bearer $api_token"
                                )
            # si el registro IPv6 existe
            dns_record_aaaa_ip=$(echo $dns_record_aaaa_id | jq -r '{"result"}[] | .[0] | .content')
            if [ $dns_record_aaaa_ip != $ipv6 ]
            then
                # cambia el registro AAAA
                curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$(echo $dns_record_aaaa_id | jq -r '{"result"}[] | .[0] | .id')" \
                     -H "Content-Type: application/json" \
                     -H "X-Auth-Email: $email" \
                     -H "Authorization: Bearer $api_token" \
                     --data "{"type":"AAAA","name":"$dns_record","content":"$ipv6","ttl":300,"proxied":false}" \
                | jq -r '.errors'
                # escribe el resultado
                echo -e "\033[0;32m [+] El registro AAAA de $dns_record ha sido actualizado: $dns_record_aaaa_ip -> $ipv6"
            else
                echo -e "\033[0;37m [~] El registro AAAA de $dns_record es el mismo que la IPv6 actual."
            fi
        fi
    else
        echo -e "\033[0;31m [-] Hay un problema para obtener el ID de la zona (subdominio) o el correo electrónico (username). Verificar estos datos e intentar nuevamente."
    fi
else
    echo -e "\033[0;31m [-] Hay un problema con el token API. Verificar este dato e intentar nuevamente."
fi

done < /home/linkmoises/ddns-cloudflare/ddns-registros.txt

He traducido y personalizado los mensajes dentro del script, así como los comentarios. La primera vez que se ejecuta y actualiza todos los registros obtendremos una salida como esta:

1
2
3
4
5
 [+] Tú dirección IPv4 es: 201.227.63.233
 [!] No se puede obtener una dirección IPv6.
 [+] El registro A de serrano.red ha sido actualizado: 127.0.0.1 -> 201.227.63.233
 [+] El registro A de www.serrano.red ha sido actualizado: 127.0.0.1 -> 201.227.63.233
 [+] El registro A de cloud.serrano.red ha sido actualizado: 127.0.0.1 -> 201.227.63.233

Una vez estén hechos los cambios y que no haya nada que actualizar la salida será esta:

1
2
3
4
5
 [+] Tú dirección IPv4 es: 201.224.2.121
 [!] No se puede obtener una dirección IPv6.
 [~] El registro A de serrano.red es el mismo que la IPv4 actual.
 [~] El registro A de www.serrano.red es el mismo que la IPv4 actual.
 [~] El registro A de cloud.serrano.red es el mismo que la IPv4 actual.

Con esto comprobamos que el script funciona adecuadamente. Ahora es momento de configurar la tarea con ayuda de cron. Abrimos crontab -e y creamos la tarea:

1
*/5 * * * * /home/usuario/ddns-cloudflare/ddns-cloudflare.sh > /dev/null 2>&1

De este modo, el script se ejecuta cada 5 minutos buscando cambios en la IP y si los encuentra actualiza los registros. Posteriormente añadí un bot de Telegram que vía webhook me notifica cada vez que hay un cambio.


Moisés Serrano Samudio Médico de atención primaria, fotógrafo aficionado, apasionado de las tecnologías relacionadas con el EdTech y el eHealth y diseñador/desarrollador de sitios web de salud. Médico, apasionado del EdTech/eHealth y diseñador/desarrollador de sitios web de salud.
Moisés Serrano Samudio

@linkmoises

Médico de atención primaria, fotógrafo aficionado, apasionado de las tecnologías relacionadas con el EdTech y el eHealth.

Entradas relacionadas

  1. Aún no hay comentarios...

Deja una respuesta

Su email no será publicado. Required fields are marked *