Dynamic DNS: One script to rule them all

Hi,

I see many scripts for different providers or situations. This script is aimed to work as many situations and providers as possible, no matter if you are behind a NAT or have a public IP. As a plus, you get a “Force update every X days” feature.

If you think this script is useful, I can add other providers as well.

Some snippets of code were taken from existing scripts published in the wiki

#************************************************************************************************
# Parameters
#************************************************************************************************
:local username "<your username>"
:local password "<your password>"
:local hostname "<subdomain.yourprovider.net>"

#availabe options: "http" or "iface"
# - http: will query an external server and discover you public ip (userful for NATted connections)
# - iface: will use the ip address assigned to the $iface interface (see below)

:local discoverBy "iface"

# interface used to get ip address from (only if discoverBy = iface)

:local iface "public-dsl"

# current available services: "dyndns", "noip" and "changeip"
:local service "changeip"

# number of days to force an update if your IP did not change (helps keeping your account active)
:local forceUpdate 15

#************************************************************************************************
# do not change below this unless you know what you are doing
#************************************************************************************************

:local force
:global lastUpdate
:local currentIP

:if ($discoverBy="http") do={ 
   /tool fetch mode=http address="checkip.dyndns.org" src-path="/" dst-path="/dyndns.checkip.html"
   :local result [/file get dyndns.checkip.html contents]
   :local resultLen [:len $result]
   :local startLoc [:find $result ": " -1]
   :set startLoc ($startLoc + 2)
   :local endLoc [:find $result "</body>" -1]
   :set currentIP [:pick $result $startLoc $endLoc]
} else={
   :set currentIP [ /ip address get [find interface=$iface disabled=no] address ]
   :for i from=( [:len $currentIP] - 1) to=0 do={
      :if ( [:pick $currentIP $i] = "/") do={ :set currentIP [:pick $currentIP 0 $i] } 
   }
}

#get IP from DynDNS for our hostname
:local resolvedIP [:resolve $hostname]

# get current date in format mm/DD/YYYY
:local date [ /system clock get date ]

# convert to YYYYMMDD
:local months ("jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec");
:local month [ :pick $date 0 3 ]; :local day [ :pick $date 4 6 ]; :local year [ :pick $date 7 11 ];
:local mm ([ :find $months $month -1 ] + 1);
:if ($mm < 10) do={ :set month ("0" . $mm); } else={ :set month $mm; }
:set date ($year . $month . $day);

:if ([ :typeof $lastUpdate ]=[:nothing] || (($date-$lastUpdate) >= $forceUpdate  && $forceUpdate > 0)) do={ 
   :set force true 
}

:put ("Current IP: $currentIP ($discoverBy), Last update: $lastUpdate")

# Determine if dyndns update is needed
:if (($currentIP != $resolvedIP) || ($force = true)) do={
    
    :if ($service = "dyndns") do={
       /tool fetch user=$username password=$password mode=http address="members.dyndns.org" \
            src-path="/nic/update?hostname=$hostname&myip=$currentIP" dst-path="/output.txt"
    }
    :if ($service = "noip") do={
       /tool fetch user=$username password=$password mode=http address="dynupdate.no-ip.com" \
            src-path="/nic/update?hostname=$hostname&myip=$currentIP" dst-path="/output.txt"
    }
    :if ($service = "changeip") do={
       /tool fetch user=$username password=$password mode=http address="nic.changeip.com" \
            src-path="/nic/update?hostname=$hostname&myip=$currentIP" dst-path="/output.txt"
    }
	
    :local result [/file get output.txt contents]
    :log info ("dynamic-dns-updater: Service = $service, Hostname = $hostname")
    :log info ("dynamic-dns-updater: CurrentIP = $currentIP, Resolved IP = $resolvedIP")
    :log info ("dynamic-dns-updater: Update result: ".$result)
    /ip dns cache flush
    :set lastUpdate $date
}

Im afraid to tell you that it doesnt work on 5.22 version of RouterOS.

Thanks anyway!!!

What is the error?

Run in console (/system script run script-name) and paste the output here pls

Thanks works great rb751 5.24

This script works perfect on 6.2 version, no problem updating dyndns host :smiley:

DNS O Matic uses the exact same syntax as DynDNS: http://dnsomatic.com/wiki/api

I added the following to the script:

    :if ($service = "dnsomatic") do={
       /tool fetch user=$username password=$password mode=http address="updates.dnsomatic.com" \
            src-path="/nic/update?hostname=$hostname&myip=$currentIP" dst-path="/output.txt"
    }

and got

status: failed

failure: closing connection: <301 Moved Permanently> 67.215.92.215:80 (4)

in the Terminal output. I assume that’s a HTTP 301 return from “updates.dnsomatic.com”? Any thoughts?

Excelent job!

Enviado desde mi XT890 usando Tapatalk 2

Figured I’d update my progress on this. I used Packet Sniffer to get a capture at the WAN to see what was going on. The DNS O Matic server is definately sending a 301 response, and redirects me to

http://www.opendns.com/nic/update?

Here’s the GET Request from the Mikrotik:

And the 301 Response from the DNS O Matic server:

So I edited the URL portion of the script to opendns.com so as to send

opendns.com/nic/update?hostname=$hostname&myip=$currentIP

and took another packet capture. The Mikrotik sends the request exactly as anticipated, and the server again responds with the exact same 301 Moved response. What’s interesting is I can take either the updates.dnsomatic.com or the dns.com URL string, throw it in my computer’s browser and get a Good response back.

Finally, for kicks and grins, I took a capture of the script updating my DynDNS account. The exact same request and URL structure from the Mikrotik, but this time a 200 OK Good response from DynDNS’s server.

So conclusion at this point is it has something to do with OpenDNS’s API server. I sent their support email a quick rundown with these screenshots, hopefully I’ll get a reply soon. I’ll post once I get a response. If anyone has any other suggestions I’d love to hear them.

This script work on 6.2 ? !!!

Yes, it works on 6.2

OpenDNS got back to me today. Basically, because the server is shared and the Mikrotik does not send a Host: in the header, the server sends a redirect to HTTPS (they do not allow updates via HTTP any longer), and another redirect to updates.opendns.com. The latter only updates your OpenDNS account, not DNS-O-Matic.

So the question is, is there a way to get the Mikrotik to send

Host:updates.dnsomatic.com

in the header of the request?

:confused: In my rb2011 6.2 with dyndns already worked on 6.1 now don’t work. The log said the last IP its the same, no update need. For 6.2 and dyndns account ? Only paste and nothing more ?

I have dyndnses on more than one interface.. any idea how to update them all, not just the one on the default gateway?
one would have to send the request out by another than the default gateway, maybe using routing marks?
On pfsense, life was so easy :wink:

Regards
Patrick

Patrick,
I’m sure there’s a more efficient way to do this in the existing script, but a method you could try would be to create two copies of the script, and set the second script up with your second DynDNS account information and set it to obtain by the appropriate Interface. You’ll have to create a second Scheduler event to make it run around the same time as the first one.

If I had any better scripting experience, I’d put something together for you; it can’t be that hard to do what you’re asking in a single script.

A nice way to do it would be based on comments on the interfaces..

Either that or based on an array…

Sent from my SCH-I545 using Tapatalk 4

I wrote two versions... I didn't actually test these. But they are an example of using either the comment on the interface or an array of hostnames to update. These only allow IP Identification by Interface Address. If you wanted to allow it via fetch and you were using HTTP (NOT HTTPS), you could use a layer7 filter to match update requests for specific hostnames and then add routing marks... that way specific hostnames request through specific interfaces.

I don't have enough hardware to setup a test to try out how that would work...

This is the Interface Comment Version:
#.* by RouterOS

Dynamic DNS Master Script

Set Interface Comment = "++hostname"

Where service = dyndns | noip | changeip

Variables

:local dyndnsUsername ""
:local dyndnsPassword ""

:local noipUsername ""
:local noipPassword ""

:local changeipUsername ""
:local changeipPassword ""

:local forceUpdate false
:local flushDNSCache true

:local forceUpdateTime 15

Script

:global lastUpdate

Date Calculations

:local date [ /system clock get date ]
:local months ("jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec");
:local month [ :pick $date 0 3 ]; :local day [ :pick $date 4 6 ]; :local year [ :pick $date 7 11 ];
:local mm ([ :find $months $month -1 ] + 1);
:if ($mm < 10) do={ :set month ("0" . $mm); } else={ :set month $mm; }
:set date ($year . $month . $day);

:if ([:typeof $lastUpdate] = "nothing" || (($date-$lastUpdate) >= $forceUpdate && $forceUpdate > 0)) do={
:set forceUpdate true
}

/interface ethernet {
:local hadUpdate false;

:foreach i in [peer find comment~"^([^+]*)\\+([^+]*)\\+([^+]*)\$"] do={
	:local commentString [get $i comment]	
	:local interfaceName [get $i name]


	:local tcomment [:pick $peerComment ([:find $peerComment "+"]+1) [:len $peerComment]]
	:local ddnsService [:pick $tcomment 0 ([:find $tcomment "+"])]
	:local ddnsHostname][ [:pick $tcomment ([:find $tcomment "+"]+1) [:len $tcomment]]

	:local ddnsAddress [:resolve $ddnsHostname]
	:local currentAddress [/ip address get [/ip address find interface=$interfaceName disabled=no] address]
	:set currentAddress [:pick $currentAddress 0 [:find $currentAddress "/" -1]]

	:local elseIf true

	:if (($force = true) || ($currentAddress != $ddnsAddress)) do={
		:if ($ddnsService = "dyndns") do={
   			/tool fetch user=$username password=$password mode=http address="members.dyndns.org" \
        	src-path="/nic/update?hostname=$ddnsHostname&myip=$currentAddress" dst-path="/output.txt"
				
			:set elseIf false
		}
		
		:if ($elseIf && $ddnsService = "noip") do={
   			/tool fetch user=$username password=$password mode=http address="dynupdate.no-ip.com" \
        	src-path="/nic/update?hostname=$ddnsHostname&myip=$currentAddress" dst-path="/output.txt"

			:set elseIf false
		}

		:if ($elseIf && $service = "changeip") do={
   			/tool fetch user=$username password=$password mode=http address="nic.changeip.com" \
        	src-path="/nic/update?hostname=$ddnsHostname&myip=$currentAddress" dst-path="/output.txt"
			
			:set elseIf false
		}

		:set hadUpdate true
	}
}
	
:if ($hadUpdate && $flushDNSCache) do={
	/ip dns cache flush
}

}This is the Array Version:
#.* by RouterOS

Dynamic DNS Master Script

Variables

:local hostnamesToUpdate {
"hostname, service, interface";
"hostname, service, interface" }

:local dyndnsUsername ""
:local dyndnsPassword ""

:local noipUsername ""
:local noipPassword ""

:local changeipUsername ""
:local changeipPassword ""

:local forceUpdate false
:local flushDNSCache true

:local forceUpdateTime 15

Script

:global lastUpdate

Date Calculations

:local date [ /system clock get date ]
:local months ("jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec");
:local month [ :pick $date 0 3 ]; :local day [ :pick $date 4 6 ]; :local year [ :pick $date 7 11 ];
:local mm ([ :find $months $month -1 ] + 1);
:if ($mm < 10) do={ :set month ("0" . $mm); } else={ :set month $mm; }
:set date ($year . $month . $day);

:if ([:typeof $lastUpdate] = "nothing" || (($date-$lastUpdate) >= $forceUpdate && $forceUpdate > 0)) do={
:set forceUpdate true
}

:local hadUpdate false;

:foreach i in $hostnamesToUpdate do={
:local tempArray [:toarray $i]

:local ddnsHostname [:pick $temparray 0]
:local ddnsService [:pick $temparray 1]
:local interfaceName [:pick $temparray 2]

:local ddnsAddress [:resolve $ddnsHostname]
:local currentAddress [/ip address get [/ip address find interface=$interfaceName disabled=no] address]
:set currentAddress [:pick $currentAddress 0 [:find $currentAddress "/" -1]]

:local elseIf true

:if (($force = true) || ($currentAddress != $ddnsAddress)) do={
	:if ($ddnsService = "dyndns") do={
   		/tool fetch user=$username password=$password mode=http address="members.dyndns.org" \
       	src-path="/nic/update?hostname=$ddnsHostname&myip=$currentAddress" dst-path="/output.txt"
		:set elseIf false
	}
		
	:if ($elseIf && $ddnsService = "noip") do={
   		/tool fetch user=$username password=$password mode=http address="dynupdate.no-ip.com" \
       	src-path="/nic/update?hostname=$ddnsHostname&myip=$currentAddress" dst-path="/output.txt"
		:set elseIf false
	}

	:if ($elseIf && $service = "changeip") do={
   		/tool fetch user=$username password=$password mode=http address="nic.changeip.com" \
       	src-path="/nic/update?hostname=$ddnsHostname&myip=$currentAddress" dst-path="/output.txt"	
		:set elseIf false
	}

	:set hadUpdate true
}

:if ($hadUpdate && $flushDNSCache) do={
/ip dns cache flush
}

I have just seen your replies and haven’t tested them yet, but thank you very much for your help!

While having a very quick review of the code (I don’t really know that scripting language yet…) I’m not sure if it addresses my main problem:

Say I have 2 WAN Links, WAN1 and WAN2. To correctly update the IP of WAN2, the fetch/get whatever command would have to get routed through WAN2, do the scripts do that?

Regards
Patrick

No… unfortunately you cannot control the interface a fetch/get goes out of. If you want to use fetch or get to identify two different interfaces then what you would have to do is to use some sort of Layer7 filtering or something to tag the requests and use routing tags to get the fetch requests to go through specific routes. I’d have to think about the correct way to do it… but it is much more complex.

There isn’t an option on fetch to force it to use a specific interface for its request.

The two scripts I wrote above… one uses an array to tell the script which interfaces to use and the other just uses comments on the interfaces. Both assume that they can get the correct IP address off the interface itself.

Do you need to use fetch to get the correct addresses?

I can think about how to do it to see, if that is what you need… But I “THINK”… you could do something like this…

Make a Layer7 filter rule to set the routing mark on packets to http://myip.dnsomatic.com/interface1 and then use the routing mark to route over interface 1… same for interface 2…

But that would require a little more thought and experimentation.

One has the correct address on the interface, that’s easy. But others do double-nat, so the interface just has an internal IP and you have to request the public IP (like the scripts do…)

Before I used a pfsense based router, there it was very easy, you could just set up several dyndns clients, one for every interface ,)
But then, Mikrotik is of course much more powerful, when you know what you’re doing (which I don’t, yet :wink: )