Exploration of Mikrotik variables. Script for updating Dynamic DNS records FreeDNS.afraid.org

    I use Mikrotik as a home and office router, and overall I really like the system. RouterOS has wide capabilities that cover 90% of my tasks, if something is missing, then you can "add" the functionality using internal scripts. But when you start writing a more or less sane script or trying to understand and apply someone else’s recipe, the outlines of the iceberg become noticeable, strange features of the language pop up.

    I did a little research on variables in Mikrotik scripts, examined ad and initialization under a magnifying glass.
    It turned out, in my opinion, a worthy topic for writing an article. So let's get started.

    What does Manual: Scripting tell us about variables in scripts? And he tells us that variables come in two scopes: local and global, which they are declared with commands

    :: local
    and
    : global

    I suggest immediately focusing on global variables, since they are easier to study due to their better "observability", and most of the conclusions, I think, can be easily transferred to local.

    Before we declare the first global variable, let's look at the features of some of the built-in data types and character constructs that can be used as variable values ​​or arguments of comparison operators.

    Manual: Scripting tells us that there are about a dozen types, of which we will consider only a few that are important for understanding variables. So, explicit and logical types: numeric num , string str and array array . The following is mentioned (it is also the value of the variable) nil, about which it is written that the variable will have it by default, if nothing is assigned to it. Believe it.

    If you work with the command console in WinBox, you will notice that there is another strange keyword nothing , which is unclear what it means, some kind of "nothing."

    [admin@MikroTik] > :global var0        
    [admin@MikroTik] > :put [:typeof $var0]
    nothing
    [admin@MikroTik] > /environment print  
    var0=[:nothing]
    

    Well, where is nil ? But full of nothing .

    Research nil


    In general, to deal with these special types and values, we apply a systematic approach, we analyze the issuance of the same type of requests, but with different sets of source data. And at the beginning we will have different “strange” constants and expressions as the initial data. The result is a table like this:
    No.What is it?: if ( = ) do = {: put TRUE} else = {: put FALSE}: put [: typeof ]: put [: typeof []]: put [: len ]: put [: len []]
    1Nothing in the command wrapper[] / [: nothing] / or even so [:][]TRUEnilnil00
    2Empty line"""" / {}TRUEstrexpected command name00
    3Nothing inside an expression(: nothing) / or even so (:)[] / "" / {}Falsenothingnil00
    4Array of nothing{: nothing} / {:}{} / ""TRUEarraynil11
    5Array element nothing({: nothing} -> 0)[] / "" / {}Falsenothingnil00

    Here we give the results of comparing different constants of expressions and commands over constants with each other. Understanding the properties of constants will help when writing the right-hand sides of comparison operators.

    At the expense of the table fields. In the Mikrotik scripting language, square brackets indicate that the result of the command is embedded in the general expression, so in the general sense, value! = [Value], this is important.

    I will comment line by line:

    Line 1: all options for writing the value field are synonymous! In the future, I suggest using concise [] . Again, square brackets indicate embedding the result of a command in a common expression. As you can see, this is one of the ways to “generate” nil not in its pure form, but as a result of an empty command in square brackets, which is exactly equal tonil .

    Line 2:
    It's simple. An empty line is an obvious thing. The only moment that it turns out to be equal to an array from nothing, but I propose not to pay special attention to this. And you can’t use an empty line as a command in square brackets, this is the only place where the terminal did not swallow the syntax for the original data of the table, it is logical.

    Line 3:
    The parentheses contain an expression, you can also put nothing inside , and the result from such an expression will also be nothing , in fact it is almost pure nothing . It can be seen that the type of the result of the expression (: nothing) gives nothing , and the type from the wrapper from the command brackets []gives nil , by analogy with line 1. In general, after re-comprehension of what was written, I realized that this was speculation, because you can put anything inside () , any meaningless set of characters, the main thing is that the result (...) gives nothing .

    Line 4:
    An array that contains nothing actually contains 1 element. The type, as expected, is array , the type of the result of the command wrapper also gives nil

    Line 5:
    We get to the element of the array from nothing, we can trace the complete analogy of the output with line 3. In general, such an element is also pure nothing .

    What general conclusions can be drawn from this strange brainfuck.nothing really exists, they can be operated on, rare expressions can return it, but commands in [] never return nothing , but only nil . Another more important conclusion is that the operator of the length of a value or the number of elements in an array : len behaves very stably and generates a predictable result, so I can definitely recommend it for use in scripts when it is necessary to check the values ​​returned by expressions and commands. And that [] = [: nothing] = nil .

    The table gives an idea of ​​what various commands and expressions of the Mikrotik language can return.

    Examining Variable Declarations


    Now let's move on to a more practical variable declaration.
    No.What is it?Ad: put : if ( = ) do = {: put TRUE} else = {: put FALSE}: put [: typeof ]: put [: typeof []]: put [: len ] it is: put [: len []]/ environment print
    1Without assignment: global var1$ var1[] / (:)False / truenothingnil0var1 = [: nothing]
    2Assignment Deleting a Variable: global var2 (: nothing)$ var2(:)TRUEnothingnil0-
    3Assigning nil: global var3 []$ var3[]TRUEnilnil0var3 = []
    4Blank line assignment: global var4 ""$ var4"" / {}TRUE / TRUEstrstr0var4 = ""
    5 (3)Strange nil , analog 3: global var5 [{}]$ var5[{}] / []TRUE / TRUEnilnil0var5 = []
    6Array of nothing: global var6 {:}$ var6"" / {""} / {}TRUE / TRUE / TRUEarrayarray1var6 = {[: nothing]}
    7 (6, 8)Array from an empty string: global var7 {""}$ var7"" / {""} / {}TRUE / TRUE / TRUEarrayarray1var7 = {""}
    8 (6, 7)Array of nil: global var8 {[]}$ var8"" / {""} / {}TRUE / TRUE / TRUEarrayarray1var8 = {[]}
    No.What is it?Ad: put : if ( = ) do = {: put TRUE} else = {: put FALSE}: put [: typeof ]: put [: typeof []]: put [: len ] it is: put [: len []]/ environment print
    9Number assignment: global var9 123$ var9n / a.123n / anumnum3var9 = 123
    10Line assignment: global var10 "987"$ var10n / a.987n / astrstr3var10 = "987"
    elevenArray of one number: global var11 {555} /: global var11 {555;}$ var11n / a.555n / aarrayarray1var11 = {555}
    12Array of dissimilar elements: global var12 {33; "test123"}$ var12n / a.33; test123n / aarrayarray2var12 = {33; "test123"}
    thirteenArray element- // -($ var12-> 0)n / a.33n / anumnum2- // -
    14Array element- // -($ var12-> 1)n / a.test123n / astrstr7- // -
    fifteenArray with nothing element: global var13 {33; (:)}$ var13n / a.33;n / aarrayarray2var13 = {33; [: nothing]}
    16Array element- // -($ var13-> 0)n / a.33n / anumnum2- // -
    17Array element- // -($ var13-> 1)n / a.n / anothingnil0- // -
    18Array with nil element: global var14 {1012; []}$ var14n / a.1012;n / aarrayarray2var14 = {1012; []}
    19Array element- // -($ var14-> 1)n / a.n / anilnil0- // -

    n / a. - not used; n / a - not applicable

    Row-wise comments on the table:

    1. The variable was created, but nothing was assigned to it. The variable as if contains nothing .
    2. If you assign such an expression to a variable, this will remove the global variable from the environment variables.
    3. The standard way to create empty variables. The variable contains nil .
    4. Assignment of an empty line. Everything is obvious here.
    5. It turns out that such an expression is similar to [] a simple assignment of nil , as in 3. I think this is because inside [] a nonexistent command and the result of this command gives nil
    6, 7, 8. Assigning curly braces makes the array an array, albeit empty. Note that the records have the same results in the table, but this does not apply to the properties of the elements of these arrays. The properties of array elements are discussed below.
    9, 10. Simple data types. Everything is pretty obvious.
    11. An array of one element, note that {555;} according to the results is {555}
    12, 13, 14. Elements of different data types may be included in the array. Investigation of the properties of array elements gives predictable results.
    15, 16, 17. One of the elements of the nothing array . Array elements have the same properties as just variables and constants of given types and values. There is an analogy with paragraph 2.
    18, 19. There is an analogy with paragraph 3. The

    study, in my opinion, turned out to be a bit controversial, but I really hope it will bring more order to your understanding of Mikrotik than chaos. As an additional compensation, I publish a script for working with dynamic DNS of the wonderful service FreeDNS.afraid.org.

    Script for FreeDNS.afraid.org


    I saw several similar scripts, but I didn’t like them because of different restrictions, so I decided to build my bike, which would completely suit me.

    As a basis, I took a script from LESHIYODESSA. I didn’t really like his algorithm, in which a file was used to store the current addresses of Dynamic DNS records and it was periodically parsed, in addition, the script does not support updating different records, it is proposed to multiply the script, but this does not solve the problem of updating the record for a given IP to the address. Therefore, in fact, I wrote my own script in which I replaced the work with files with a more reliable mechanism for periodic updates (with an hourly interval) and forced updates to change the monitored IP addresses of interfaces received via DHCP, independent of several entries.

    We declare arrays of names of subdomains of FreeDNS.afraid.org and their hashes, names of WAN interfaces, from which we will track IP addresses. And also set the number of records (Quant) by the size of the array or manually:

    :local SubdomainHashes {"U3dWVE5V01TWxPcjluEo0bEtJQWjg5DUz=";"U3pWV5VFTWxPcjlOEo0EtJpOE1MAyDc="}
    :global DNSDomains {"aaa.xyz.pu";"bbb.xyz.pu"}
    :global WANInterfaces {"ether4-WAN-Inet";"ether3-WAN-Beeline"}
    :global Quant [:len $DNSDomains]
    

    We declare auxiliary variables:

    SkipCounters - an array of verification counters of monitored interfaces.
    LastIPs - an array of IP addresses that have already been sent to FreeDNS.

    An array of counters allows you to update Dynamic DNS records independently of each other.

    First, I make an empty variable declaration : global SkipCounters , such a declaration allows you to either create a new global variable, or use an existing variable in the environment variables and its value without overwriting.

    The following constructions work for newly created variables, the data type is checked, if it is not an array, the type changes to an array, and the values ​​of the variables are assigned. Thus, at the output, we have array-type variables initialized with the necessary values.

    :global SkipCounters
    :if ([:typeof $SkipCounters] != "array") do={
    :set SkipCounters {""}
    :for i from=0 to=($Quant-1) do={:set ($SkipCounters->$i) 1}
    }
    :global LastIPs
    :if ([:typeof $LastIPs] != "array") do={
    :set LastIPs {""}
    :for i from=0 to=($Quant-1) do={:set ($LastIPs->$i) ""}
    }
    

    Neither the tracking-update algorithm itself.

    Get the current IP address from dhcp-client. Then the most interesting.

    The [/ ip dhcp-client get [find where interface = ($ WANInterfaces -> $ i)] address] command can generally return anything but a string containing an IP address, so it will normally update the value of the CurrentIP variable. The returned value can be either a string with IP or nil, or there will be a runtime error and the command will not update CurrentIP. Therefore, I enter the explicit declaration line above : local CurrentIP "" . And after executing the command, CurrentIP will either have a "", or nil, or an IP address.

    As I wrote above, the operator: len has the most stability, so we use it further to check the adequacy of the data[: len $ CurrentIP]> 0 . We also monitor the value of the counter, and if it is> = 60, forcibly send the request to FreeDNS. Thus, the stability of the algorithm to communication problems is increased. I run the script in the sheduler once a minute, so the mandatory update period is about 1 hour, which does not burden the FreeDNS service much.

    What else is worth paying attention to. Parameter "& address =" is present in the URL of the update request. $ CurrentIP, this parameter allows you to explicitly specify the IP address for the subdomain instead of the automatic one (on the interface from which the request was sent).

    :for i from=0 to=($Quant-1) do={
    :local CurrentIP ""
    :set CurrentIP [/ip dhcp-client get [find where interface=($WANInterfaces->$i)] address]
    :set CurrentIP [:pick $CurrentIP 0 ([:len $CurrentIP]-3)]
    # :log info ("Current SkipCounter$i: ".($SkipCounters->$i))
    :if ([:len $CurrentIP] > 0 and ($CurrentIP != ($LastIPs->$i) or ($SkipCounters->$i) > 59)) do={
    :if ($CurrentIP != ($LastIPs->$i)) do={
    :log info ("Service Dynamic DNS: Renew IP: ".($LastIPs->$i)." for ".($DNSDomains->$i)." to $CurrentIP")
    }
    /tool fetch url=("http://freedns.afraid.org/dynamic/update.php\?".($SubdomainHashes->$i)."&address=".$CurrentIP) keep-result=no
    :set ($LastIPs->$i) $CurrentIP
    :set ($SkipCounters->$i) 1
    } else={
    :set ($SkipCounters->$i) (($SkipCounters->$i) + 1)
    }
    }
    

    Entire MultiFreeDNS Script
    # MultiFreeDNS
    :local SubdomainHashes {"U3dWVE5V01TWxPcjluEo0bEtJQWjg5DUz=";"U3pWV5VFTWxPcjlOEo0EtJpOE1MAyDc="}
    :global DNSDomains {"aaa.xyz.pu";"bbb.xyz.pu"}
    :global WANInterfaces {"ether4-WAN-Inet";"ether3-WAN-Beeline"}
    :global Quant [:len $DNSDomains]
    :global SkipCounters
    :if ([:typeof $SkipCounters] != "array") do={
    :set SkipCounters {""}
    :for i from=0 to=($Quant-1) do={:set ($SkipCounters->$i) 1}
    }
    :global LastIPs
    :if ([:typeof $LastIPs] != "array") do={
    :set LastIPs {""}
    :for i from=0 to=($Quant-1) do={:set ($LastIPs->$i) ""}
    }
    :for i from=0 to=($Quant-1) do={
    :local CurrentIP ""
    :set CurrentIP [/ip dhcp-client get [find where interface=($WANInterfaces->$i)] address]
    :set CurrentIP [:pick $CurrentIP 0 ([:len $CurrentIP]-3)]
    # :log info ("Current SkipCounter$i: ".($SkipCounters->$i))
    :if ([:len $CurrentIP] > 0 and ($CurrentIP != ($LastIPs->$i) or ($SkipCounters->$i) > 59)) do={
    :if ($CurrentIP != ($LastIPs->$i)) do={
    :log info ("Service Dynamic DNS: Renew IP: ".($LastIPs->$i)." for ".($DNSDomains->$i)." to $CurrentIP")
    }
    /tool fetch url=("http://freedns.afraid.org/dynamic/update.php\?".($SubdomainHashes->$i)."&address=".$CurrentIP) keep-result=no
    :set ($LastIPs->$i) $CurrentIP
    :set ($SkipCounters->$i) 1
    } else={
    :set ($SkipCounters->$i) (($SkipCounters->$i) + 1)
    }
    }
    


    Also popular now: