To make my self-hosted services more reliable while I’m travelling, I’ve migrated some critical services to run directly on my router. By offloading these tasks from my server, I remove a point of failure (server outages) from affecting the network. I use a Ubiquiti EdgeRouter X (ER-X) at home, and was inspired to make this change by Wesley Brewer’s post about running AdGuard Home directly on the router. In addition to running AdGuard Home on the router, I also moved my dynamic DNS service from Home Assistant’s Cloudflare integration to a short bash script. Running these services directly on the router ensures that my network is internet accessible, even if my server goes down - unless of course the router also goes down, but then I wouldn’t have internet anyway!
The Cloudflare API
The script uses the Cloudflare v4 API, specifically the PUT endpoint for updating an existing DNS record:
https://api.cloudflare.com/client/v4/zones/<zone_id>/dns_records/<record_id>
To authenticate with this endpoint you’ll need a Cloudflare API token. For dynamic DNS you need a token with Zone:Zone:Read and Zone:DNS:Edit permissions. Cloudflare’s documentation covers the full token creation process at developers.cloudflare.com/fundamentals/api/get-started/create-token/.
For each DNS record you want to update, you’ll also need a “Zone ID” and “Record ID”. First find the zone ID on the Overview page for your domain (right-hand sidebar under API). The record ID can be retrieved via the API (it’s not shown in the dashboard). In the terminal, use this curl command (replacing <your_zone_id> and <your_api_token>) to list all of the records (and their IDs) for that zone.
curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones/<your_zone_id>/dns_records" \
-H "Authorization: Bearer <your_api_token>" \
-H "Content-Type: application/json"With the API token, Zone ID(s), and Record ID(s) in hand, we can use the Cloudflare API to update DNS records.
curl -s -X PUT \
"https://api.cloudflare.com/client/v4/zones/<your_zone_id>/dns_records/<your_record_id>" \
-H "Authorization: Bearer <your_api_token>" \
-H "Content-Type: application/json" \
--data "<json_formatted_record_data>"The script below uses this API to automatically update DNS records via the Cloudflare API whenever the router’s public IP changes.
Dynamic DNS records
The script works in three steps:
Get the current public IP
The IP is read directly from the router’s WAN interface using
ip addrrather than querying an third-party IP-echo service (for both speed and reliability).Compare IP against the cached IP
The current public IP is compared with the last IP that this script updated Cloudflare with. Storing the previous IP in a local cache file removes the need for API calls or DNS requests.
Update Cloudflare IP
If the IP has changed, the script uses the above
PUTrequest to the Cloudflare API to update the outdated DNS records. The last IP local cache is only written once the API reports a success, so if the request fails it will be retried when the script is next run.
/config/scripts/update-cloudflare-ddns.sh [59 lines]
#!/bin/vbash
source /opt/vyatta/etc/functions/script-template
# --- USER CONFIG ---
CF_API_TOKEN="<your_api_token>"
WAN_IF="eth0" # WAN interface with your public IPv4
CF_TTL=120 # TTL in seconds
CACHE_FILE="/config/scripts/cloudflare-ddns-ip.cache"
# One entry per record:
# "ZONE_ID,RECORD_ID,NAME,TYPE,PROXIED"
# TYPE is usually A (IPv4) or AAAA (IPv6)
# PROXIED is true or false
RECORDS=(
"<your_zone_id>,<your_record_id>,<your_record_name>,A,false"
)
# -------------------
# Get current IPv4 from WAN interface
current_ip=$(ip -4 addr show "$WAN_IF" | awk '/inet / {print $2}' | cut -d/ -f1)
[ -z "$current_ip" ] && exit 0
# Compare with cached value
if [ -f "$CACHE_FILE" ]; then
cached_ip=$(cat "$CACHE_FILE")
else
cached_ip=""
fi
# If unchanged, nothing to do
if [ "$current_ip" = "$cached_ip" ]; then
exit 0
fi
# Update all records
all_ok=1
for rec in "${RECORDS[@]}"; do
IFS=',' read -r zone_id rec_id rec_name rec_type rec_proxied <<< "$rec"
# Only handle IPv4 A records here
if [[ "$rec_type" != "A" ]]; then
continue
fi
update_response=$(curl -s -X PUT \
"https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$rec_id" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"type\":\"$rec_type\",\"name\":\"$rec_name\",\"content\":\"$current_ip\",\"ttl\":$CF_TTL,\"proxied\":$rec_proxied}")
if ! echo "$update_response" | grep -q '"success":true'; then
all_ok=0
fi
done
# Only write cache if all updates succeeded
if [ "$all_ok" -eq 1 ]; then
echo "$current_ip" > "$CACHE_FILE"
fiTo use this script, replace <your_api_token> with your API token and enter the records to keep updated in the RECORDS array. This script allows multiple records across several zones to be updated, simply add an entry for each record into the RECORDS array. Finally, we’ll also need to make this script executable.
chmod +x /config/scripts/update-cloudflare-ddns.shThe task scheduler on the ER-X can be used to regularly run this script - I’ve set the script to run every couple minutes. Since the IP rarely changes, the vast majority of runs will quickly hit the cache check and exit immediately (requiring no API calls, no network traffic).
configure
set system task-scheduler task cloudflare-ddns executable path /config/scripts/update-cloudflare-ddns.sh
set system task-scheduler task cloudflare-ddns interval 2m
commit
save
exitCitation
@online{oharawild2026erxdns,
author = {O’Hara-Wild, Mitchell},
title = {Dynamic {DNS} on an {EdgeRouter} {ER‑X} with {Cloudflare}},
date = {2026-03-18},
url = {https://mitchelloharawild.com/blog/erx-cloudflare-dns/},
langid = {en}
}