Community discussions

MikroTik App
 
nocivo
just joined
Topic Author
Posts: 4
Joined: Fri Jun 26, 2020 1:12 pm
Location: japan
Contact:

DDNS Cloudflare script

Mon Mar 18, 2024 9:51 am

Hello everyone,

I would like to preface by saying that I am not a programmer and I need help solving the following issue. I have written the following script for RouterOS that updates the IP in a given DNS record on Cloudflare when the router's IP changes. I started from the script I had previously written for Gandi's liveDNS. The script works well with "no proxied" records. I wanted to make it work with "proxied" records as well, but obviously, resolving the IP using 1.1.1.1 or any other public DNS returns not the original IP but the IP of the proxy. Therefore, I need to make a query using Cloudflare's APIs and extract the registered IP to compare it with the new one. If they are the same, I do nothing; if it is new, I update the record. This is the script:
# ------Cloudflare DDNS script licensed under GPL 3 write by Massimo Ciani ----------
#
#
#
#--------------- Change Values in this section to match your setup ------------------

# Your Clouflare API token variables. Generate in your cloudflare website area
:local cfToken "...................................."

# work with only one root domain or subdomain.
# if you want to manage multiple domain or subdomain clone the script and change based on new domain
# policy read, write and test
# IMPORTANT: Before to start the script, remember to create manually the records for all domain. I think.
# ZoneID is inside the website area in the cloudflare user area
:local cfZoneId "................................"
##### To obtain cfDnsId use following command in any unix shell:
#####     curl -X GET "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/dns_records" -H "X-Auth-Email: YOUR_EMAIL" -H "X-Auth-Key: YOUR_API_KEY" -H "Content-Type: application/json" | python -mjson.tool
:local cfDnsId "................................"
:local domain "sub.example.com"
:local dnsType "A"
:local dnsTTL 300
:local dnsProxied false
:local resolver "1.1.1.1"

# Set the name of interface where get the internet public IP
:local inetinterface "ether1"

#------------------------------------------------------------------------------------
# No more changes need

# get current IP
:global currentIP
:if ([/interface get $inetinterface value-name=running]) do={
    :global currentIPa [/ip address get [find interface="$inetinterface" disabled=no] address]
    # cancel netmask from interface IP address
    :for i from=( [:len $currentIPa] - 1) to=0 do={
        :if ( [:pick $currentIPa $i] = "/") do={ 
            :set currentIP [:pick $currentIPa 0 $i]
       } 
   }
} else={
    :log info "Cloudflare: $inetinterface is not currently running, so therefore will not update."
    :error [:log info "bye"]
}

# Resolve domain and update on IP changes. This method work only if use non proxied record.
#:global previousIPb [:resolve domain-name="$domain" server="$resolver"]

#### find a way to extract ip from the record using the cloudflare API. needed if you want to use proxied record ####
:local getApiUrl "https://api.cloudflare.com/client/v4/zones/$cfZoneId/dns_records/$cfDnsId"
:local authHeader "content-type:application/json,Authorization:Bearer $cfToken"
:local jsonData [/tool fetch mode=https http-method=get url="$getApiUrl" http-header-field="$authHeader" as-value]
#### extract IP from the result query. but don't work
:global previousIPb ($jsonData->"result"->"content")
#####################################################################################################################

:log info "RecordIP is $previousIPb"

:if ($currentIP != $previousIPb) do={
    :log info "Cloudflare $domain: Current IP $currentIP is not equal to previous IP, update needed"
    :log info "Cloudflare $domain: Sending update"
    # 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
    :local headers "content-type:application/json,Authorization:Bearer $cfToken"
    :local payload "{\"type\":\"$dnsType\",\"name\":\"$domain\",\"content\":\"$currentIP\",\"ttl\":$dnsTTL,\"proxied\":$dnsProxied}"
    /tool fetch mode=https http-method=put url="$apiUrl" http-header-field="$headers" http-data="$payload" dst-path="" output=none
    :log info "Cloudflare $domain: updated on Cloudflare with IP $currentIP"
} else={
    :log info "Cloudflare $domain: Previous IP $previousIPb is equal to current IP, no update needed"
}
To make a query using Cloudflare's APIs, I use the fetch command and everything works fine; I can download the data of the record in JSON format.
:local getApiUrl "https://api.cloudflare.com/client/v4/zones/$cfZoneId/dns_records/$cfDnsId"
:local authHeader "content-type:application/json,Authorization:Bearer $cfToken"
:local jsonData [/tool fetch mode=https http-method=get url="$getApiUrl" http-header-field="$authHeader" as-value]
Unfortunately, however, I cannot extract the IP from the JSON data downloaded with fetch, using the following code.
:global previousIPb ($jsonData->"result"->"content")
The field I am interested in extracting is the "result"->"content" field inside the JSON data. It always returns an empty field.

I have read the RouterOS scripting documentation, but I have not been able to find a method that works with my limited programming knowledge.

Could someone help me?

Thank you
 
trkk
just joined
Posts: 17
Joined: Tue Mar 08, 2022 1:36 pm
Location: +09:00

Re: DDNS Cloudflare script

Tue Mar 19, 2024 6:14 am

JSON is not "native" data type in RouterOS. So we have to convert JSON to native array before using the data. (Or simply treat it as string)

If you are using ROS 7.13 or later, you can use ":deserialize" command
:put ([:deserialize from=json value=$jsonData]->"result"->"content")
Or else, you have to use JSON parser script, like https://github.com/Winand/mikrotik-json-parser .
# https://github.com/Winand/mikrotik-json-parser
:global JSONLoads
:put ([$JSONLoads $jsonData]->"result"->"content")
 
nocivo
just joined
Topic Author
Posts: 4
Joined: Fri Jun 26, 2020 1:12 pm
Location: japan
Contact:

Re: DDNS Cloudflare script

Wed Mar 20, 2024 4:01 pm

Hello,

Thank you for your suggestion. I solved the issue by removing the fetch command from inside the variable jsonData and instead writing the fetch result to a file. I'm not entirely sure why, but it seems that a variable only accepts string, int, float, or an array, not JSON data? I had attempted using the deserialize command before, but it didn't work with fetch inside the variable.

The current working code is as follows:
# ------Cloudflare DDNS script licensed under GPL 3 write by Massimo Ciani ----------
#
#                                   v. 1.0
#                  Works with RouterOS versions 7.13 and above.
#
#--------------- Change Values in this section to match your setup ------------------

# Your Clouflare API token variables. Generate in your cloudflare website area
:local cfToken ""

# work with only one root domain or subdomain.
# if you want to manage multiple domain or subdomain clone the script and change based on new domain
# policy read, write, ftp and test
# IMPORTANT: Before to start the script, remember to create manually the records for domain or subdomain.
# ZoneID is inside the cloudflare user area.
:local cfZoneId ""

##### To obtain cfDnsId use following command in any unix shell:
##### curl -X GET "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/dns_records" -H "X-Auth-Email: YOUR_EMAIL" -H "X-Auth-Key: YOUR_API_KEY" -H "Content-Type: application/json" | python -mjson.tool
:local cfDnsId ""

:local domain "example.com or sub.example.com"
:local dnsType "A"
:local dnsTTL 1
:local dnsProxied true
:local resolver "1.1.1.1"

# Set the name of interface where get the internet public IP
:local inetinterface "ether1"

#------------------------------------------------------------------------------------
# No more changes need

# get current IP
:global currentIP
:if ([/interface get $inetinterface value-name=running]) do={
    :global currentIPa [/ip address get [find interface="$inetinterface" disabled=no] address]
    # cancel netmask from interface IP address
    :for i from=( [:len $currentIPa] - 1) to=0 do={
        :if ( [:pick $currentIPa $i] = "/") do={ 
            :set currentIP [:pick $currentIPa 0 $i]
       } 
   }
} else={
    :log info "Cloudflare: $inetinterface is not currently running, so therefore will not update."
    :error [:log info "bye"]
}

:global previousIPb
# Resolve domain and update on IP changes. This method work only if use non proxied record.
#:set previousIPb [:resolve domain-name="$domain" server="$resolver"]

#### download json of the DNS record using the cloudflare API. ####
:local getApiUrl "https://api.cloudflare.com/client/v4/zones/$cfZoneId/dns_records/$cfDnsId"
:local authHeader "content-type:application/json,Authorization:Bearer $cfToken"
/tool fetch mode=https http-method=get url="$getApiUrl" http-header-field="$authHeader" output=file
#### extract IP from the json DNS record file.
:local fileContent [/file get "$cfDnsId" contents]
:set previousIPb ([:deserialize from=json value=$fileContent]->"result"->"content")
:log info "Previous IP is $previousIPb"

:if ($currentIP != $previousIPb) do={
    :log info "Cloudflare $domain: Current IP $currentIP is not equal to previous IP, update needed"
    :log info "Cloudflare $domain: Sending update"
    # 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
    :local headers "content-type:application/json,Authorization:Bearer $cfToken"
    :local payload "{\"type\":\"$dnsType\",\"name\":\"$domain\",\"content\":\"$currentIP\",\"ttl\":$dnsTTL,\"proxied\":$dnsProxied}"
    /tool fetch mode=https http-method=put url="$apiUrl" http-header-field="$headers" http-data="$payload" dst-path="" output=none
    :log info "Cloudflare $domain: updated on Cloudflare with IP $currentIP"
} else={
    :log info "Cloudflare $domain: Previous IP $previousIPb is equal to current IP, no update needed"
}

Thanks
 
User avatar
Larsa
Forum Guru
Forum Guru
Posts: 1068
Joined: Sat Aug 29, 2015 7:40 pm
Location: The North Pole, Santa's Workshop

Re: DDNS Cloudflare script

Wed Mar 20, 2024 7:31 pm

Hello @nocivo! If you want to explore similar solutions to figure out how they work, you can search for mikrotik Cloudflare script on github.

Who is online

Users browsing this forum: Bing [Bot] and 6 guests