MikroTik DDNS with Cloudflare API v4
I’ve been using NowDNS to provide name resolution to my home network for quite some time. On top of that, I use Cloudflare to manage DNS for this and several other domains. I took advantage of both by pointing out some DNS records that need dynamic IP to NowDNS’s DDNS-domain from Cloudflare.
After I browsed the Cloudflare APIs, I discovered that they provide an API to update the DNS. I could utilize that by using RouterOS scripting, I thought. Yup, there are many RouterOS Cloudflare DDNS scripts on the internet, but mostly use the old deprecated API, so I decided to make my own using the latest API(v4).
Prerequisites
We’ll have to use this particular Update DNS Record API endpoint. To use it, we need to use at least RouterOS v6.44 for /tool fetch
to support the http-header-field
parameter, and three things from Cloudflare:
- API Token
- Zone Identifier
- DNS Identifier
In addition, the MikroTik router must have a WAN interface that has a dynamic public IP address.
Getting API Token
- Go to the Cloudflare Token Management page
- Create Token, use the “Edit zone DNS” template
- Adjust with your needs, then Continue to Create Token
Getting Zone Identifier
- Go to one of your domain dashboards on Cloudflare
- You should find it on the sidebar, right side of the page
Getting DNS Identifier
In this step, we need more effort to get the DNS Identifier. We have to get it from an API response, the List DNS Records endpoint.
Have you created an API Token and found your Zone Identifier yet? We’ll use that in the following API request.
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_IDENTIFIER/dns_records?type=A&name=$DNS_NAME" \
-H "Authorization: Bearer $API_TOKEN"
*DNS_NAME is the domain that will have a dynamic IP, ex: myddns.bayukurnia.com. If you don't have yet, create a new one from Cloudflare Dashboard.
The Script
Yay, the fun part. But before we get started, let’s make some goals for the final script.
I want my script to have the following behavior:
- Make API request only if IP changed
- Logs only if it’s necessary, saving more log space
- Logs error details when DDNS fails
Let’s jump into the script. The first thing we’re going to do is to set some variables.
Variables
# global variables
# we'll update it on every ddns success
:global currentIp
# outgoing interface
:local wanInterface "indihome"
# cloudflare variables, adjust with yours
:local cfToken "aLszWiLWxxxxxxxxxxxxxxxxxxxxxx"
:local cfZoneId "1a4ee9660xxxxxxxxxxxxxxxxxxxxxxx"
:local cfDnsId "6b0e74xxxxxxxxxxxxxxxxxxxxxxxx"
:local dnsType "A"
:local dnsName "myddns.bayukurnia.com"
:local dnsTTL "1"
:local dnsProxied "false"
There are several dynamic variables based on the current router state. We need a few commands to set these variables.
Getting Current WAN Interface IP Address
Here’s MikroTik scripting examples page.
# get current $wanInterface IP
:local newIpCidr [/ip address get [find interface="$wanInterface"] address ]
# strip out netmask notation
:local newIp [:pick $newIpCidr 0 [:find $newIpCidr "/"]]
Composing API Variables
Go to Update DNS Record documentation if you want a clear explanation about the payload value constraints.
# compose endpoint
# docs: https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record
:local apiUrl "https://api.cloudflare.com/client/v4/zones/$cfZoneId/dns_records/$cfDnsId"
# headers & payload
:local headers "Authorization: Bearer $cfToken"
:local payload "{\"type\":\"$dnsType\",\"name\":\"$dnsName\",\"content\":\"$newIp\",\"ttl\":$dnsTTL,\"proxied\":$dnsProxied}"
The Main Thing
Due to make sure that our flow will execute only if the IP has changed, we’ll wrap the rest of the script inside an if
condition. If this condition doesn’t meet, the script will exit without additional overhead.
- Make API request only if IP changed âś…
:if ($newIp != $currentIp) do={
...
}
I wanted to log an API error response if the request failed until I realized that the RouterOS script would throw an error if /tool fetch
’s HTTP response didn’t succeed, which broke the script.
After some browsing, I learned that the only way to handle these error are using the runtime error catch block, but still, there’s no way to get the error response. I feel a bit disappointed since we’ll log the error without the details.
- Logs error
detailswhen DDNS fails âś…
:do {
:local response [/tool fetch http-method="put" url=$apiUrl http-header-field=$headers http-data=$payload as-value output=user]
...
} on-error {
:log error "DDNS: failed to change IP $currentIp to $newIp"
}
We’ll log if only the request succeeded, then update the currentIp
variable.
- Logs only if it’s necessary ✅
:if ($response->"status" = "finished") do={
:log info "DDNS: changed $currentIp to $newIp"
# update $currentIp with the new one
:set currentIp $newIp
}
Final Script
Please note that I’m not an expert in this field. If you intend to use the script on production, you should test it yourself. There might be some bugs that I don’t realize yet. 👻
We have our final script with all the goals fulfilled. I think.
- Make API request only if IP changed âś…
- Logs only if it’s necessary ✅
- Logs error
detailswhen DDNS fails âś…
#--------------------------------------------
# MikroTik DDNS Script | Cloudflare API v4
# bayukurnia.com
#--------------------------------------------
# global variables
# we'll update it on every ddns success
:global currentIp
# outgoing interface
:local wanInterface "indihome"
# get current $wanInterface IP
:local newIpCidr [/ip address get [find interface="$wanInterface"] address ]
:local newIp [:pick $newIpCidr 0 [:find $newIpCidr "/"]]
:if ($newIp != $currentIp) do={
# cloudflare variables, adjust with yours
:local cfToken "aJV8sCQqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
:local cfZoneId "fb36edd6xxxxxxxxxxxxxxxxxxxxxxxx"
:local cfDnsId "afc8b34dxxxxxxxxxxxxxxxxxxxxxxxx"
:local dnsType "A"
:local dnsName "myddns.bayukurnia.com"
:local dnsTTL "1"
:local dnsProxied "false"
# compose endpoint
# docs: https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record
:local apiUrl "https://api.cloudflare.com/client/v4/zones/$cfZoneId/dns_records/$cfDnsId"
# compose headers & payload
:local headers "Authorization: Bearer $cfToken"
:local payload "{\"type\":\"$dnsType\",\"name\":\"$dnsName\",\"content\":\"$newIp\",\"ttl\":$dnsTTL,\"proxied\":$dnsProxied}"
# make API request
:do {
:local response [/tool fetch http-method="put" url=$apiUrl http-header-field=$headers http-data=$payload as-value output=user]
:if ($response->"status" = "finished") do={
:log info "DDNS: changed $currentIp to $newIp"
# update $currentIp with the new one
:set currentIp $newIp
}
} on-error {
:log error "DDNS: failed to change IP $currentIp to $newIp"
}
}
Conclusion
As a Cloudflare user, I’m happy they provide comprehensive APIs even for free-tier users like me. Thus there’s no need to use additional DDNS service. Hooray 🎉