Background
I have, for a time, been using CloudFlare for CDN, Optimizations and DNS Management for this and a few other domains. At the same time, I’ve been using DynDNS to provide name resolution to my home network/lab. Browsing around the CloudFlare JSON API I noticed that I can update DNS records through some fairly simple HTTP GET requests, and since the RouterOS (the operating system used by Mikrotik’s routerboards) has support for some pretty decent scripting I decided to let my router update CloudFlare for some custom and free dynamic DNS resolution.
Attribution
My current script is a modified version of a script developed by Konstantin Antselovich.
Original script: http://konstant1n.livejournal.com/9759.html
Original Author website: http://konstantin.antselovich.com/
Thanks!
How you do it
What you need
- RouterOS v6+ for HTTPS support
- Cloudflare API key, aka “Token”, found at the account page
- Cloudflare DNS Zone name
- Cloudflare Record Id (more on that later)
- Cloudflare subdomain name
- Name of the external router interface
Getting the Record Id
To get the record id of the DNS record you want to update, I used the rec_load_all function of the API. And since I’m a windows admin, I use powershell for that.
$cfDomain = "homelab"
$cfToken = "<your API Key/Token>"
$cfZone = "example.com"
$cfEmail = "<your CF email address>"
$apiLoadAllRecordsURI = "https://www.cloudflare.com/api_json.html?a=rec_load_all&tkn=$cfToken&email=$cfEmail&z=$cfZone"
$restRequest = Invoke-RestMethod -Uri $apiLoadAllRecordsURI
$restRequest.response.recs.objs | ?{$_.display_name -eq $cfDomain} | fl -Property rec_id,name,display_name,type,ttl,service_mode
That should, given that you’ve replaced all the example variables produce something like this:
rec_id : 16606003
name : homelab.example.com
display_name : homelab
type : A
ttl : 1
service_mode : 0
And with that done, you should have everything necessary for the RouterOS script.
Building the Script
I’m not going to go into the syntax of ros scripts , but I’ll try to explain the different building block to some degree.
Defining our variables
First, we’ll define out variables used for the script. The global $hostname variable is actually redundant as I could derive the FQDN from $CFDomain and $CFZone, but it’s used in other scripts in my router so it’s included for that purpose alone.
######## Set and collect general variables #########
:global hostname "homelab.example.com"
:global resolvedIP ""
:global externalIP ""
:global WANInterface "eth1-ext-internet"
######## Set CloudFlare variables #################
:local CFemail "<same email as in the powershell script"
:local CFtkn "<your API key/token>"
:local CFzone "example.com"
:local CFid "16606003"
:local CFtype "A"
:local CFttl "1"
:local CFservicemode "0"
:local CFDomain "homelab"
:local CFDebug "true"
Take care to verify and compare the CFxx variables with the ones used in and received from the previous powershell script, but also the $hostname variable.
- hostname: Used in the script to lookup the currently set IP-address.
- WANInterface: The list name of the external interface, used to find the external IP-address.
- CFemail: Your CloudFlare Login email.
- CFtkn: Your API Key/Token.
- CFzone: Your DNS Zone.
- CFid: The rec_id from the powershell script.
- CFtype: A for IPv4 host records, AAAA for IPv6.
- CFttl: Record time-to-live, use “1” for automatic.
- CFservicemode: Use “0” for direct traffic or “1” to go through their CDN, Optimizations and Security features. I recommend “0” for anything but websites.
- CFDomain: Subdomain or rather hostname for your router.
- CFDebug: Use “True” to print some informational stuff to the router logs.
Resolve IP-addresses
Next, we resolve two IP-addresses. First one, $externalIP, is the routers current external address. And the second, $resolvedIP, is the address currently set for our $hostname.
######## Resolve and set IP-variables ##########
:local currentIP [/ip address get [/ip address find interface=$WANInterface ] address];
:set externalIP [:pick $currentIP 0 [:find $currentIP "/"]];
:set resolvedIP [:resolve $hostname];
Build CF API Request and log debug info
Now, build the URI for the API call from our variables.
######## Build CF API Url #########################
:local CFurl "https://www.cloudflare.com/api_json.html\3F"
:set CFurl ($CFurl . "a=rec_edit&tkn=$CFtkn&id=$CFid");
:set CFurl ($CFurl . "&email=$CFemail&z=$CFzone&type=$CFtype");
:set CFurl ($CFurl . "&name=$CFDomain&service_mode=$CFservicemode&ttl=$CFttl");
And if the $CFdebug variable is set to “true”, write a little info to the router log.
######## Write debug info to log #################
:if ($CFDebug = "true") do={
:log info ("CF: hostname = $hostname")
:log info ("CF: resolvedIP = $resolvedIP")
:log info ("CF: currentIP = $currentIP")
:log info ("CF: externalIP = $externalIP")
:log info ("CF: CFurl = $CFurl&content=$externalIP")
};
Compare and Update
Last step is to compare $externalIP with $resolvedIP. If they don’t match, our external IP-address has been changed and we need to update the DNS record. To execute the HTTP GET request we use a RouterOS tool called fetch to which we pass the $CFurl variable. The reason this will not work before RouterOS v6+ is that before that there was no support for HTTPS, and CloudFlare does not accept unencrypted traffic for their APIs.
The script block is pretty straight-forward. Compare, update, flush local DNS-cache and write to the log.
######## Compare and update CF if necessary #####
:if ($resolvedIP != $externalIP) do={
:log info ("CF: Updating CF, setting $CFDomain = $externalIP")
/tool fetch mode=https url="$CFurl&content=$externalIP" keep-result=no
/ip dns cache flush
} else={
:log info "CF: No Update Needed!"
}
Permissions/Policies
This part, I’m not entirely sure on. The script needs to do name resolution, read settings and set variables and it works for me with these policies configured:
- Read
- Write
- Test
- Sniff
- Sensitive
It might be over-doing it a bit, but I have not had occasion to fiddle with them yet.
Scheduling
I have this script scheduled to run once every 20 minutes. As it will only try to update the DNS records if there is a difference between them and the current external IP-address I feel fairly sure that I won’t be tagged as a spammer or something by CloudFlare.
The schedule is configured with:
- Start Time: startup
- Interval: 00:20:00
- On Event:
/system script run <Name of your script> - Policy: Read,Write, Test, Sniff, Sensitive
This schedule is also a good place for a hairpin update if it’s needed.
The Copy-Paste part
Now, obviously, at least make sure you proof-read the script before putting it into production. I am not a network dude, and I rarely dabble with ROS scripts. I am certain that anyone with a bit more experience could create a smarter, prettier version of this script, so use at your own risk. Anyway, here’s the script in it’s entirety.
######## Set and collect general variables #########
:global hostname "homelab.example.com"
:global resolvedIP ""
:global externalIP ""
:global WANInterface "eth1-ext-internet"
######## Set CloudFlare variables #################
:local CFemail "<same email as in the powershell script"
:local CFtkn "<your API key/token>"
:local CFzone "example.com"
:local CFid "16606003"
:local CFtype "A"
:local CFttl "1"
:local CFservicemode "0"
:local CFDomain "homelab"
:local CFDebug "true"
######## Resolve and set IP-variables ##########
:local currentIP [/ip address get [/ip address find interface=$WANInterface ] address];
:set externalIP [:pick $currentIP 0 [:find $currentIP "/"]];
:set resolvedIP [:resolve $hostname];
######## Build CF API Url #########################
:local CFurl "https://www.cloudflare.com/api_json.html\3F"
:set CFurl ($CFurl . "a=rec_edit&tkn=$CFtkn&id=$CFid");
:set CFurl ($CFurl . "&email=$CFemail&z=$CFzone&type=$CFtype");
:set CFurl ($CFurl . "&name=$CFDomain&service_mode=$CFservicemode&ttl=$CFttl");
######## Write debug info to log #################
:if ($CFDebug = "true") do={
:log info ("CF: hostname = $hostname")
:log info ("CF: resolvedIP = $resolvedIP")
:log info ("CF: currentIP = $currentIP")
:log info ("CF: externalIP = $externalIP")
:log info ("CF: CFurl = $CFurl&content=$externalIP")
};
######## Compare and update CF if necessary #####
:if ($resolvedIP != $externalIP) do={
:log info ("CF: Updating CF, setting $CFDomain = $externalIP")
/tool fetch mode=https url="$CFurl&content=$externalIP" keep-result=no
/ip dns cache flush
} else={
:log info "CF: No Update Needed!"
}