Shout-out to all the selfhosters around there!
Ever thought about running your own dynamic DNS system without relying on third-party providers? Well, me too, not going to lie.
Luckily, if you own a domain and have a CloudFlare account, there’s a few things we can do about that. Today, we are bringing a bunch of different approaches to face this scenario, so let’s get down to it.
General assumptions
- Have your own domain
- Set up your domain’s DNS with CloudFlare
- Have a CloudFlare account with DNS management capabilities
- This task works with IPv4 only (for now)
Classic Linux cronjob with a simple cURL
Specific assumptions
- Have access to a Linux host (no root permissions needed)
- Have the
jq
package installed
Being this one the simplest way to achieve our goal, just save the following script to a file:
DNS_ZONE='<your DNS zone ID here>'
DNS_RECORD='<your DNS record ID here>'
AUTH_KEY='<your CloudFlare API token here>'
EMAIL_ADDRESS='<your CloudFlare's email account here>'
DNS_RECORD_NAME="\"<your.domain.tld here>\""
CURRENT_IP_ADDRESS="\"$(curl -s ip.me)\""
CURRENT_DNS_VALUE=$(curl -sX GET "https://api.cloudflare.com/client/v4/zones/${DNS_ZONE}/dns_records/${DNS_RECORD}" -H "Content-Type:application/json" -H "X-Auth-Key:${AUTH_KEY}" -H "X-Auth-Email:${EMAIL_ADDRESS}" | jq '.result["content"]')
if [ ${CURRENT_DNS_VALUE} != ${CURRENT_IP_ADDRESS} ]; then
curl -sX PUT "https://api.cloudflare.com/client/v4/zones/${DNS_ZONE}/dns_records/${DNS_RECORD}" -H "X-Auth-Email:${EMAIL_ADDRESS}" -H "X-Auth-Key:${AUTH_KEY}" -H "Content-Type:application/json" --data '{"type":"A","name":'${DNS_RECORD_NAME}',"content":'${CURRENT_IP_ADDRESS}'}' > /dev/null
fi
Let’s say we name this file as cf-ddns.bash
and save it to /opt
. Then make it executable:
chmod +x /opt/cf-ddns.bash
And now let’s create a cron job that will run it every hour. Open the crontab editor with:
crontab -e
Then simply add the following line:
@hourly /bin/bash /opt/cf-ddns.bash
It is worth mentioning that the Linux machine where you are setting up this task must be behind the same NAT or on the same local network from where you want your DNS record to point to its public IP address.
Kubernetes cronjob
Specific assumptions:
- Run a Kubernetes cluster (pretty obvious right?)
Why not!?
If you are running a self-hosted Kubernetes cluster you can just go all the way down and make use of the native cronjob feature it provides.
In this case you would only have to apply the following manifest, which creates a pod that will run the previous script we defined but it will be managed at a cluster level. (don’t forget to update the variable values according to yours!) :
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: cf-dyndns
namespace: default
spec:
schedule: "@hourly"
successfulJobsHistoryLimit: 0
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
containers:
- name: cf-dyndns
image: bash:latest
imagePullPolicy: IfNotPresent
restartPolicy: OnFailure
command:
- /usr/local/bin/bash
- -c
- apk add --update curl jq &>/dev/null ; DNS_ZONE='<your DNS zone ID here>' DNS_RECORD='<your DNS record ID here>' AUTH_KEY='<your CloudFlare API token here>' EMAIL_ADDRESS='<your CloudFlare account's email here>' DNS_RECORD_NAME="\"<your.domain.tld here>\"" CURRENT_IP_ADDRESS="\"$(curl -s ip.me)\"" CURRENT_DNS_VALUE=$(curl -sX GET "https://api.cloudflare.com/client/v4/zones/${DNS_ZONE}/dns_records/${DNS_RECORD}" -H "Content-Type:application/json" -H "X-Auth-Key:${AUTH_KEY}" -H "X-Auth-Email:${EMAIL_ADDRESS}" | jq '.result["content"]'); if [ ${CURRENT_DNS_VALUE} != ${CURRENT_IP_ADDRESS} ] ; then curl -sX PUT "https://api.cloudflare.com/client/v4/zones/${DNS_ZONE}/dns_records/${DNS_RECORD}" -H "X-Auth-Email:${EMAIL_ADDRESS}" -H "X-Auth-Key:${AUTH_KEY}" -H "Content-Type:application/json" --data '{"type":"A","name":'${DNS_RECORD_NAME}',"content":'${CURRENT_IP_ADDRESS}'}' > /dev/null; fi && echo OK || echo ERROR
You can always check its status by running:
kubectl get cronjob
Drone CI pipeline
Specific assumptions:
- Run a Drone CI instance
- A git repository to interact with through Drone
Another way to automate your DynDNS setup could be defining the task into a CI pipeline, and for that purpose this time we will be using Drone CI to do so.
Although Drone provides a specific plugin to interact with CloudFlare’s API with ease, in this case we will be using a basic cURL image so we can get the DynDNS logic working in a pipeline inside our .drone.yml
file.
There is a very specific reason for this: as we are dealing with dynamic IP addresses that may change over time, there are some limitations with Drone when it comes to sharing variable between steps in a pipeline.
Thus the workaround we are proposing here to bypass that limitation consists of having the ability to assign to the CURRENT_IP_ADDRESS
variable a value at the very time the same pipeline’s step is running, not before.
In other words, not inheriting the contents from a pipeline-level variable but to gather its value from inside the same container that will also handle its content to update our DNS record itself on the next command it runs:
---
kind: pipeline
type: kubernetes # or "docker" if you don't have a kubernetes runner configured!
name: cf-ddns
steps:
- name: Update DNS record
image: curlimages/curl
environment:
AUTH_KEY:
from_secret: cloudflare_token
EMAIL_ADDRESS:
from_secret: cloudflare_email
DNS_ZONE_ID:
from_secret: cloudflare_zone_id
DNS_RECORD_ID:
from_secret: cloudflare_record_id
DNS_RECORD_NAME: <your.domain.tld here>
commands:
- export CURRENT_IP_ADDRESS="$(curl -s ip.me)"
- curl -sX PUT "https://api.cloudflare.com/client/v4/zones/$DNS_ZONE_ID/dns_records/$DNS_RECORD_ID" -H "X-Auth-Email:$EMAIL_ADDRESS" -H "X-Auth-Key:$AUTH_KEY" -H "Content-Type:application/json" --data "{\"type\":\"A\",\"name\":\"$DNS_RECORD_NAME\",\"content\":\"$CURRENT_IP_ADDRESS\"}
trigger:
event:
- cron
As you can see we are here using secrets to provide the multiple pipeline-level variable contents too, so don’t forget to create those secrets manually inside the repository options through the Drone web interface.
Once that’s done, the last thing we need is to create the cron job that will trigger the pipeline. The cron job options are easily found in the web UI as well, but if you’d rather using the cli you can always run something like this:
drone cron add "myuser/myrepo" "cf-ddns" '@hourly'
Worth to point out, this last method gets rid of the original principle: “if the IP address has changed, then overwrite the DNS record with the current IP address” that was present on the first two examples, and just runs the HTTP request to overwrite the DNS record with whatever IP address it gathers each time, no matter if it is a new one or not.
That’s all for now. Wish it was useful!
Cheers.