This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Dynamic

If you host something out in residential land you’ll need this. Your IP changes periodically and you don’t want to have track that manually.

1 - Cloudflare

CF provides an excellent DDNS service and all you need is curl. Pretty simple, right?

ZONE_ID=
RECORD_ID=
TOKEN=

IP=$(curl -4 --silent --show-error --fail --max-time 10 https://api.ipify.org)

curl --silent --show-error --fail --max-time 10 \
                -X PATCH \
                "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
                -H "Authorization: Bearer $TOKEN" \
                -H "Content-Type: application/json" \
                --data "{\"content\":\"$IP\"}")

Who needs ddclient!

Noice! Done!

OK - well, you need to know your IDs, and you have to set up a token that only has DNS rights to the specific zone in question. And you need to handle errors, so I guess you need something more like…

vi ~/ddns-update
#!/bin/bash

set -euo pipefail

# Configuration
ZONE="your.org"
RECORD="server.your.org"
TOKEN="XXXXXXX"

# State dirs (systemd uses /var/lib/SERVICENAME)
STATE_DIR="/var/lib/cloudflare-ddns"
ZONE_ID_FILE="$STATE_DIR/zone_id"
RECORD_ID_FILE="$STATE_DIR/record_id"
LAST_IP_FILE="$STATE_DIR/last_ip"

# Load or fetch zone and record IDs needed for CF's API
if [ -f "$ZONE_ID_FILE" ]; then

    ZONE_ID=$(<"$ZONE_ID_FILE")

else

    ZONE_ID=$(curl --silent --show-error --fail --max-time 10 \
        -H "Authorization: Bearer $TOKEN" \
        -H "Content-Type: application/json" \
        "https://api.cloudflare.com/client/v4/zones?name=$ZONE" \
        | jq -r '.result[0].id')

    echo "$ZONE_ID" > "$ZONE_ID_FILE"
fi

[ -n "$ZONE_ID" ] && [ "$ZONE_ID" != "null" ] || {
    echo "Failed to discover Cloudflare Zone ID"
    exit 1
    }


if [ -f "$RECORD_ID_FILE" ]; then

    RECORD_ID=$(<"$RECORD_ID_FILE")

else

    RECORD_ID=$(curl --silent --show-error --fail --max-time 10 \
        -H "Authorization: Bearer $TOKEN" \
        -H "Content-Type: application/json" \
        "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=A&name=$RECORD" \
        | jq -r '.result[0].id')

    echo "$RECORD_ID" > "$RECORD_ID_FILE"
fi
    
[ -n "$RECORD_ID" ] && [ "$RECORD_ID" != "null" ] || {
    echo "Failed to discover Cloudflare Record ID"
    exit 1
    }
 

# Load the last IP. 
LAST_IP=""
if [ -f "$LAST_IP_FILE" ]; then

    LAST_IP=$(<"$LAST_IP_FILE")
fi

# Get the current IP
IP=$(curl -4 --silent --show-error --fail --max-time 10 https://api.ipify.org)
[[ $IP =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || {
    echo "Failed validation check for data: $IP" 
    exit 1
    }

# If they aren't the same
if [ "$IP" != "$LAST_IP" ]; then

        echo "Updating IP to $IP"

        RESPONSE=$(curl --silent --show-error --fail --max-time 10 \
                -X PATCH \
                "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
                -H "Authorization: Bearer $TOKEN" \
                -H "Content-Type: application/json" \
                --data "{\"content\":\"$IP\"}")
    

        RESULT=$(echo "$RESPONSE" | jq -r '.success')

        if [ "$RESULT" = "true" ]; then
                echo "$IP" > "$LAST_IP_FILE"
                echo "Update successful"
        else
                echo "Cloudflare update failed"
                echo "$RESPONSE" | jq -r '.errors[]?.message'
                exit 1
        fi
fi

Whew, OK. Now you need to get this running somehow. Cron? Oh, right. Not in Debian 13 by default. Systemd timer it is, then.

sudo install -m 700 ddns-update /usr/local/sbin/ddns-update
sudo vi /etc/systemd/system/cloudflare-ddns.service
[Unit]
Description=Cloudflare DDNS Update
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot

StateDirectory=cloudflare-ddns
StateDirectoryMode=0700

ExecStart=/usr/local/sbin/ddns-update
sudo vi /etc/systemd/system/cloudflare-ddns.timer
[Unit]
Description=Run Cloudflare DDNS updater every 15 minutes
LogLevelMax=warning

[Timer]
OnBootSec=1min
OnUnitActiveSec=15min

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflare-ddns.timer

Queue Spongebob: Two hours later…and we’ve just made a (worse) copy of ddclient that does (a lot) less.