Running my own dynamic DNS service with cloudflare

Like for many people, the IP address of my home internet connection changes regularly. This can be annoying when you want to run a website or a VPN on your home network and want to access it from the internet. To get around this most people use a dynamic DNS service. There are many websites out there offering such a service for a small fee. Normally you install an agent on one of your computers and the agent updates a DNS record on some domain you don't control, for example home-stephan.dyndns.biz or something like this.
Since I own a couple of domains, which are hosted at cloudflare where I can modify them using an HTTP API, I wanted to do this myself, without relying on a third-party.
All I needed to do, was create a small bash script that does the following:
- Get my current public IP address of my home internet connection
- Get the currently configured IP address on my chosen subdomain DNS A record
- Check if they are different, if not exit
- If they a different, send a API request to change the A record to the new IP address
I use dig to check the first two conditions. Cloudflare offers a special domain that you can query, that always responds with the IP address the request originated from: IP4NEW=$(dig +short @1.1.1.1 whoami.cloudflare ch txt | sed "s/\"//g")
. The txt record is surrounded by " so I remove them with sed. The ch in front of the txt means to query the chaos DNS class, a relic from a long time ago, which sometimes is used for such special use cases.
I query my currently set DNS record directly from the cloudflare DNS servers at 1.1.1.1 to be sure not to get a cached response: IP4CUR=$(dig @1.1.1.1 +short {{ dyndns_domain }} A)
. Replace {{ dyndns_domain }}
with you chosen subdomain.
For the API request we need three things. The zone id of the domain you are using, the id of the DNS record you want to modify and an API token with the permission to modify the DNS records.
The zone id can be easily found on the overview page of the domain on the bottom right side under API > Zone ID.
To get the record id you need to use the API. You have to create the record beforehand with a placeholder IP address. This can be done with curl: curl 'https://api.cloudflare.com/client/v4/zones/{{ dyndns_cf_zone }}/dns_records' --header 'Content-Type: application/json' --header 'Authorization: Bearer {{ dyndns_cf_api_token }}'
. Replace {{ dyndns_cf_zone }}
with the id from step one, and {{ dyndns_cf_api_token }}
with the token from the next step. The result is a JSON that includes every record of the domain, look for the name field that includes your chosen subdomain and copy the id field.
To create an API token, open your cloudflare profile and select API Tokens. Create a new token that looks like this:

Under Zone Resources replace nuxio.de with your own domain.
To modify the record with the API I use curl. Replace all variables with {{ }} around them with your corresponding values. The documentation to update a record can be found here: https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record
The resulting bash script looks like this:
#!/bin/bash
set -e
set -u
set -o pipefail
IP4NEW=$(dig +short @1.1.1.1 whoami.cloudflare ch txt | sed "s/\"//g")
IP4CUR=$(dig @1.1.1.1 +short {{ dyndns_domain }} A)
if test -n "$IP4NEW"; then
if test "$IP4NEW" != "$IP4CUR"; then
curl -sS -X PUT "https://api.cloudflare.com/client/v4/zones/{{ dyndns_cf_zone }}/dns_records/{{ dyndns_cf_dns_record }}" \
-H "Authorization: Bearer {{ dyndns_cf_api_token }}" \
-H "Content-Type: application/json" \
--data '{"type":"A","name":"{{ dyndns_domain }}","content":"'"$IP4NEW"'","ttl":120,"proxied":false}'
fi
fi
I then use a systemd service and timer to run this script once every minute. The sandboxing feature are probably overkill, but hey, better safe than sorry.
[Unit]
Description=Update dyndns
[Service]
Type=oneshot
User=dyndns
ExecStart=/srv/dyndns/dyndns.sh
# filesystem access
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
# network
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
# misc
NoNewPrivileges=true
PrivateUsers=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
ProtectKernelLogs=true
LockPersonality=true
ProtectHostname=true
RemoveIPC=true
RestrictSUIDSGID=true
[Unit]
Description=Update dyndns
[Timer]
OnBootSec=1m
OnUnitActiveSec=1min
[Install]
WantedBy=timers.target