Writing scripts for Mikrotik RouterOS is easy

  • Tutorial
RouterOS is a Linux-based network operating system. This operating system is designed to be installed on Mikrotik RouterBoard hardware routers. Also, this system can be installed on a PC (or virtual machine), turning it into a router. Initially, the OS, which is quite rich in functionality, is no, no, and it will surprise you with the absence of any necessary chip from the box. Unfortunately, access to the Linux environment is very limited, therefore, “it is under Linux” is absolutely not equivalent to “it is in RouterOS”. But do not despair! This system provides several options for expanding its functionality. The first - the simplest and most native - is the ability to write scripts in an embedded language.
In this article, as an example, we will consider a script that converts DNS names to IP lists (address lists).
Why might it be needed? Many sites use Round Robin DNS to distribute the load (and some not only for this). To control access to such a site (create a routing or firewall rule), we need all the IP addresses corresponding to this domain name. Moreover, the list of IP addresses after the lifetime of a given DNS record (in this case we are talking about an A record) can be issued completely new, so the information will have to be updated periodically. Unfortunately, you cannot create a rule in RouterOS.
block all TCP connections on port 80 at example.com
in place of example.com there should be an IP address, but as we already understood, example.com does not correspond to one, but several IP addresses. To save us from the pain of creating and maintaining a bunch of rules of the same type, RouterOS developers made it possible to create a rule like this:
block all TCP connections on port 80 to any address in the list named DenyThis
The only thing left is to automatically create this very list. Who still has not got tired of my writings, I invite you to habrakat.

I’ll give you the text of the script right away, followed by its step-by-step analysis
:local DNSList {"example.com";"non-exist.domain.net";"server.local";"hostname"}
:local ListName "MyList"
:local DNSServers ( [ip dns get dynamic-servers], [ip dns get servers ], 8.8.8.8 )
:foreach addr in $DNSList do={
     :foreach DNSServer in $DNSServers do={
          :do {:resolve server=$DNSServer $addr} on-error={:log debug ("failed to resolve $addr on $DNSServer")}
     }
}
/ip firewall address-list remove [find where list~$ListName]
/ip dns cache all
:foreach i in=[find type="A"] do={
    :local bNew true
    :local cacheName [get $i name]
    :local match false
    :foreach addr in=$DNSList do={
       :if (:typeof [:find $cacheName $addr] >= 0) do={
           :set $match true
       }
    }
    :if ( $match ) do={
        :local tmpAddress [/ip dns cache get $i address]
        :if ( [/ip firewall address-list find ] = "") do={
            :log debug ("added entry: $[/ip dns cache get $i name] IP $tmpAddress")
            /ip firewall address-list add address=$tmpAddress list=$ListName comment=$cacheName
        } else={
            :foreach j in=[/ip firewall address-list find ] do={
                :if ( [/ip firewall address-list get $j address] = $tmpAddress ) do={
                    :set bNew false
                }
            }
            :if ( $bNew ) do={
                :log debug ("added entry: $[/ip dns cache get $i name] IP $tmpAddress")
                /ip firewall address-list add address=$tmpAddress list=$ListName comment=$cacheName
            }
        }
    }
}



The script text must be added to the script repository located in the / system scripts section.
The script is executed line by line. Each line has the following syntax:
[prefix] [path] command [uparam] [param=[value]] .. [param=[value]]
[prefix] - ":" - for global commands, the command line starts with the "/" character, which will be executed relative to the configuration root, there may be no prefix, then the command line is executed relative to the current configuration section;
[path] - the path to the desired configuration section, along which the transition occurs before executing the command;
command - directly the action performed by the command line;
[uparam] - an unnamed command parameter;
[param = [value]] - named parameters and their values.

So, first of all, we define the script operation parameters in the form of variables. The variable is declared by the commands: local and: global, respectively, we get a local variable that is available only within its scope, or global, which is added to the list of OS environment variables and will be accessible from anywhere. Local variables live while their scope is being executed, global variables - until we delete them.

:local DNSList {"example.com";"non-exist.domain.net";"server.local";"hostname"}
:local ListName "MyList"
:local DNSServers ( [ip dns get dynamic-servers], [ip dns get servers ], 8.8.8.8 )
The DNSList variable contains an array of domains that we want to work with. The variable ListName contains a string that will be called the received address-list. The DNSServers variable - contains an array of DNS server addresses registered on the router or received from the provider when connecting, plus "eights" in case the router does not use the DNS service, which will be used to obtain information about domain records.

:foreach addr in $DNSList do={
     :foreach DNSServer in $DNSServers do={
          :do {:resolve server=$DNSServer $addr} on-error={:log debug ("failed to resolve $addr on $DNSServer")}
     }
}
In the “for everyone” cycle, we’ll go around the domain array and pick up their IP addresses on each DNS server in case different DNS give different IP. Design
:do {command} on-error={command}
serves to catch runtime errors. If you do not use it, then the script may be interrupted if the resolution resolves a nonexistent or erroneous address.

/ip firewall address-list remove [find where list~$ListName]

Let's go to the / ip firewall address-list configuration section and delete all entries in which the list name contains the value of the $ ListName variable. The construction of square brackets allows one to execute another within the current command, and pass the result of the current as a parameter.

/ip dns cache all
:foreach i in=[find type="A"] do={
let's go to the / ip dns cahe all configuration section. It contains the DNS cache of the router in the form of a table Name - Type - Data - TTL. We perform a selection by type - we need only A-records. And we’ll go around the selection result in the “for everyone” cycle. This will be the main cycle of our script.

:local bNew true
:local cacheName [get $i name]
:local match false

Let's create the variables updated in each cycle: two flags - bNew, excluding duplication, match, showing whether the current cache entry is in our list of domains; the cacheName variable contains the Name field of the current cache entry, that is, the domain.

:foreach addr in=$DNSList do={
  :if (:typeof [:find $cacheName $addr] >= 0) do={
    :set $match true
  }
}

We will go around the list of domains and for each one we will check whether the substring in the cacheName line is in the form of a domain from this list.
Why not use equality comparison?
Very simple - the script logic assumes that sub-domains should be treated in the same way as domains. If we want to block social accounts, then it makes sense to block not only the main domain, but also, for example, servers that give statics, pictures, scripts, and are located on the sub-domains of this site. It will also help to avoid listing separately domains with "www" and without. The fact that these domains did not hit the cache during resolving is not scary, because they can get there when the browser resolves the user (though this requires that the user's DNS queries are processed in RouterOS).
If so, set the match flag to true.

:if ( $match ) do={
  :local tmpAddress [/ip dns cache get $i address]
  :if ( [/ip firewall address-list find ] = "") do={
    :log debug ("added entry: $[/ip dns cache get $i name] IP $tmpAddress")
    /ip firewall address-list add address=$tmpAddress list=$ListName comment=$cacheName
  } else={
    :foreach j in=[/ip firewall address-list find ] do={
      :if ( [/ip firewall address-list get $j address] = $tmpAddress ) do={
        :set bNew false
      }
    }
    :if ( $bNew ) do={
      :log debug ("added entry: $[/ip dns cache get $i name] IP $tmpAddress")
      /ip firewall address-list add address=$tmpAddress list=$ListName comment=$cacheName
    }
  }
}

In the final step, if the current address needs to be added (match is set to true), then we add it to the address list. The comment for the entry to be added will contain the domain to which it belongs. At the same time, we carry out several checks. If the address-list is empty, then add it immediately, if something is there, check to see if there is already an entry with that IP address, and if not, add it.

The list of addresses needs to be updated periodically. To do this, RouterOS has a task manager. The task can be added from the console or from the winbox GUI
/system scheduler
add interval=5m name=MyScript on-event="/system script run MyScript" policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive start-date=may/08/2014 \
    start-time=10:10:00


Scenarios for working with the address list are not limited to creating rules in the firewall. Therefore, I will give a few examples. You can execute it in the console, you can add it with the mouse in winbox.
Black list:
/ip firewall filter 
add chain=forward protocol=tcp dst-port=80 address-list=DenyThis \
    action=drop

Static route to these nodes
/ip firewall mangle
add action=mark-routing chain=prerouting dst-address-list=AntiZapret \
    in-interface=bridge_lan new-routing-mark=RouteMe
/ip route
add distance=1 gateway=172.16.10.2 routing-mark=RouteMe

Customer Information Collection
/ip firewall mangle
add action=add-src-to-address-list address-list=FUPer chain=prerouting \
    dst-address-list=Pron log=yes log-prefix=critical


List of sources:
Scripting documentation
Simple examples from RouterOS developers
Scripts added by

UPD users : specially at the request of turone, made changes to the script so that the addresses of DNS servers were taken from the system.
UPD 08/24/2016: I noticed that in the new versions of RouterOS (starting from 6.36) it became possible to specify DNS names in address lists. So now the value of this script is only educational.

Also popular now: