Dynamic DNS on Cloudflare with your own domain name and a little help of Bash script

Overview

Sometimes available ready-to-use solutions for dynamic DNS are not enough for your needs, or need to be customised, or requires some additional actions every month (i.e. confirm the hostname, like on free account of no-ip.com). If you have your own domain name you could register in cloudflare.com (it is free) and use the capability of cloudflare for Dynamic DNS. In my case I used my domain name 100.org.ua, registered in cloudflare (free account), used cloudflare’s NSs (DNS records of my domain name are managed in cloudflare.com) and small bash script which is sending the HTTP PUT request to cloudflare every time when the IP address of the host (server, where the domain name should be pointed out) is changing.

What is Dynamic DNS?

Dynamic DNS is the service which is just updating the A DNS record of your domain name (it can actually do much more), which is usually IPv4 or IPv6. Dynamic DNS is used when you don’t have a static (or white) IP address but you need to be able to reach your host (home laptop, router, NAS or server) remotely, from internet. Most of the WiFi routers and other network devices have the option to ping the Dynamic DNS when the public (internet) IP of your device/host is changed (i.e. the router is restarted and your network provider granted you a new IP address). These settings may sit under the network, security or remote access section.

How?

First of all you need to identify your new IP address, get your cloudflare zone ID, get your cloudflare host dns record ID and then send a cloudflare API request HTTP PUT to update the DNS records.

In my case I will be determining the IP address from the Google Cloud VM. And here is a Bash script sample:

#!/bin/bash

# Calculates the new IP address of the current Google Cloud VM, which is started/restarted
get_gce_ip () {
    local instance="${1%%.*}"
    gcloud compute instances list --filter="$instance" \
        --format='value(networkInterfaces[0].accessConfigs[0].natIP)'
}

# Cloudflare zone is the zone which holds the record
zone=100.org.ua
# dnsrecord is the A record which will be updated
# it can be also any subdomain
dnsrecord=100.org.ua

## Cloudflare authentication details
## keep these private, replace by your's one
API_TOKEN=qwERtyuIo3Pasd5FG8h2JklZxcvbNMq0WErTYuio

# Get the current external IP address
ip="`get_gce_ip`"

echo "Current IP is $ip"

# if here, the dns record needs updating
if host $dnsrecord 1.1.1.1 | grep "has address" | grep "$ip"; then
  echo "$dnsrecord is currently set to $ip; no change needed"
  exit
fi

# get the zone id for the requested zone
zoneid=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$dnsrecord&status=active" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" | jq -r '{"result"}[] | .[0] | .id')

echo "Zoneid for $dnsrecord is $zoneid"

# get the dns record id
dnsrecordid=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zoneid/dns_records?type=A&name=$dnsrecord" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" | jq -r '{"result"}[] | .[0] | .id')

echo "DNSrecordid for $dnsrecord is $dnsrecordid"

# update the record
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zoneid/dns_records/$dnsrecordid" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  --data "{\"type\":\"A\",\"name\":\"$dnsrecord\",\"content\":\"$ip\",\"ttl\":1,\"proxied\":false}" | jq .

echo "DNS Record A is updated for $dnsrecord"

As you can see in the example above I used such applications as host, jq, curl and gcloud compute. You will have to make sure that you installed them before you will be running this script. Also make sure that gcloud has a right permissions, you can change the permissions for gcloud API in your VM settings

How to verify?

To be able to check whether the script worked out you can check the DNS A record of your domain name, if it is updated – all good, if not – check the output of the script.

In my case I used the following command when connected to my google cloud VM by SSH:

sudo journalctl -u google-startup-scripts.service

The output was:

Apr 12 22:24:28 <InstanceID> systemd[1]: Starting Google Compute Engine Startup Scripts...
Apr 12 22:24:29 <InstanceID> GCEMetadataScripts: Starting startup scripts (version 20201214.00).
Apr 12 22:24:29 <InstanceID> GCEMetadataScripts: Found startup-script in metadata.
Apr 12 22:24:43 <InstanceID> GCEMetadataScripts: startup-script: Current IP is 12.211.34.56
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script: Zoneid for 100.org.ua is <ZoneID>
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script: DNSrecordid for 100.org.ua is <DNSRecordID>
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script: {
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:   "result": {
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "id": "763549e5244b4eb767da6835c5d95b2b",
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "zone_id": "<ZoneID>",
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "zone_name": "100.org.ua",
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "name": "100.org.ua",
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "type": "A",
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "content": "12.211.34.56",
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "proxiable": true,
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "proxied": false,
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "ttl": 1,
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "locked": false,
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "meta": {
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:       "auto_added": false,
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:       "managed_by_apps": false,
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:       "managed_by_argo_tunnel": false,
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:       "source": "primary"
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     },
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "comment": null,
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "tags": [],
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "created_on": "2023-04-12T19:28:07.154133Z",
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:     "modified_on": "2023-04-12T22:24:44.402147Z"
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:   },
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:   "success": true,
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:   "errors": [],
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script:   "messages": []
Apr 12 22:24:44 <InstanceID> GCEMetadataScripts: startup-script: }
Apr 12 22:24:45 <InstanceID> systemd[1]: google-startup-scripts.service: Succeeded.
Apr 12 22:24:45 <InstanceID> systemd[1]: Started Google Compute Engine Startup Scripts.
Apr 12 22:24:45 <InstanceID> GCEMetadataScripts[542]: 2023/04/12 22:24:45 GCEMetadataScripts: startup-script exit status 0
Apr 12 22:24:45 <InstanceID> GCEMetadataScripts[542]: 2023/04/12 22:24:45 GCEMetadataScripts: Finished running startup scripts.