As discussed in the Firewalls and DDNS post, DuckDNS can sometime be slow. Additionally, some corporate DNS resolves do not resolve the DuckDNS domains making them inaccessible. To solve this, I decided to use Cloudflare’s name servers to handle my domains. Contrary to the spirit of free open source software, when it comes to domain registration, it is not free. You may be able to find deals on domain name registration, but they’re usually promotional and will eventually make you pay. I went with Cloudflare and their domain registration services for simplicity and a single control plane since I decided I’d be using their DNS.
Creating a DDNS automation script
Create a folder in your home folder names something like .ddns
, like so mkdir -p ~/.ddns
. Create and edit a file called variables
in this .ddns
folder with your favorite text editor, I prefer nano, nano ~/.ddns/variables
.
In the variables
add your (you guessed it) variables you got from Cloudflare.
SUBDOMAIN="<subdomain.fdqn.tld>"
KEY="<key>"
ZONE_ID="<zone_id>"
You need to protect this file. This file contains information that will allow changing your DNS records in your Cloudflare account.
chmod 500 ~/.ddns
cd ~/.ddns
chmod 400 ~/.ddns/variables
chmod 500 ~/.ddns/ddns.sh
#!/bin/bash
# This script updates the Cloudflare's A DNS records for your domain.
# It requires the DNS edit token key and the DNS Zone ID.
# The script uses curl for the API calls and awk parse the responses.
# Be sure to create the subdomain A DNS record in the Cloudflare portal with a dummy routable IP address first.
# Create a file in the same folder where you place this script and name the file variables
# Put these lines in to the file variables
#SUBDOMAIN="<subdomain.fdqn.tld>"
#KEY="<key>"
#ZONE_ID="<zone_id>"
# Make sure the user who will be executing this script can read the file
# Function to validate IP address
validate_ip() {
local ip=$1
# Regular expression to match valid IPv4 addresses
local valid_ip_regex="^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$"
# Regular expressions to exclude non-routable IP addresses
local non_routable_regexes=(
"^0\.([0-9]{1,3}\.){2}[0-9]{1,3}$"
"^10\.([0-9]{1,3}\.){2}[0-9]{1,3}$"
"^100\.(6[4-9]|7[0-9]|1[0-1][0-9]|12[0-7])\.([0-9]{1,3}\.)[0-9]{1,3}$"
"^127\.([0-9]{1,3}\.){2}[0-9]{1,3}$"
"^169\.254\.([0-9]{1,3}\.)[0-9]{1,3}$"
"^172\.(1[6-9]|2[0-9]|3[0-1])\.([0-9]{1,3}\.)[0-9]{1,3}$"
"^192\.0\.0\.([0-9]{1,3})$"
"^192\.0\.2\.([0-9]{1,3})$"
"^192\.88\.99\.([0-9]{1,3})$"
"^192\.168\.([0-9]{1,3}\.)[0-9]{1,3}$"
"^198\.(1[8-9])\.([0-9]{1,3}\.)[0-9]{1,3}$"
"^198\.51\.100\.([0-9]{1,3})$"
"^203\.0\.113\.([0-9]{1,3})$"
"^224\.([0-9]{1,3}\.){2}[0-9]{1,3}$"
"^(24[0-9]|25[0-5])\.([0-9]{1,3}\.){2}[0-9]{1,3}$"
)
# Check if the IP address matches the valid IP regex
if [[ $ip =~ $valid_ip_regex ]]; then
# Check if the IP address matches any of the non-routable IP regexes
for regex in "${non_routable_regexes[@]}"; do
if [[ $ip =~ $regex ]]; then
echo "Invalid IP address: Non-routable IP address"
return 1
fi
done
echo "$ip is a valid IP address"
return 0
else
echo "Invalid IP address: Does not match IPv4 format"
return 1
fi
}
# Make sure that the ddns.sh and variables files are in the same directory
source ./variables
# Get old IP address
OLD_IP=$(host $SUBDOMAIN | awk '/has address/ { print $4 }')
if validate_ip "$OLD_IP"; then
echo "Old IP for $SUBDOMAIN is $OLD_IP"
else
echo "Failed getting old IP"
exit 1
fi
# Get new IP address
NEW_IP=$(curl -s --connect-timeout 20 https://checkip.amazonaws.com)
if validate_ip "$NEW_IP"; then
echo "New IP for $SUBDOMAIN is $NEW_IP"
else
echo "Failed getting new IP"
exit 1
fi
# Compare the IP addresses
if [ "$NEW_IP" == "$OLD_IP" ]; then
echo "IP is already correct"
else
# Get Subdomain DNS Record from Cloudflare
DNS_RECORD_ID=$(curl -s --connect-timeout 20 --request GET \
--url https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $KEY" \
| awk -v RS='{"' -F: '/^id/ && /'"$SUBDOMAIN"'/{print $2}' | tr -d '"' | sed 's/,.*//')
echo "Got Record ID"
# Update IP Address
curl -s -o /dev/null --connect-timeout 20 --request PUT \
--url https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$DNS_RECORD_ID \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $KEY" \
--data "{\"type\":\"A\",\"name\":\"$SUBDOMAIN\",\"content\":\"$NEW_IP\",\"ttl\":1,\"proxied\":false}"
echo "IP address for $SUBDOMAIN is updated to $NEW_IP"
fi
exit 0
This script queries DNS record for your domain and compares the IP address with the one obtained from https://checkip.amazonaws.com
. If they match, it does nothing. If they don’t match, the script updates the Cloudflare’s DNS record. The script uses curl for the API calls and awk to parse the responses.
Now create a cron job (with crontab -e
command) to run this script every <X>
minutes (adjust to taste). Change <user>
to the username where you stored the script.
*/<X> * * * * /home/<user>/.ddns/ddns.sh >/dev/null 2>&1
Obtaining the DNS token
To get the Zone ID and the API key, go to your Cloudflare portal, select the domain you need. In the overview scroll down and select “Click to copy” underneath the Zone ID field and paste it into the variables
file for the ZONE_ID
variable. Then click “Get your API token” to create your API token.

Now, click on the blue “Create Token” button.

Next, click the “Use template” button next to the “Edit zone DNS”.

There edit the Token Name. It’ll be important to keep track of all your tokens if you create many. In the “Zone Resources” select “Specific zone” and select the domain you’re creating the token for. And then click “Continue to summary button”.


Not much to do here but to click the “Create Token” button.

This is where you finally get your token. Make sure you copy it into the variables
file for the KEY
variable as you won’t be able to view it again and will need to recreate it.
This is it. This is the method I use to track my IP addresses for the devices with the dynamic public IPs. I then query the DNS record to get the IP and update the allow lists on my firewall (as described in Firewalls and DDNS). Of course, there’s a delay in the DNS record propagation, plus the frequency at which you run the script, so it may not be fully suitable for the mission critical applications. But if you’re running a mission critical application, you’re probably not relying on dynamic IPs and use a static IP anyways…