MikroTik RouterOS: how to use hostnames in firewall rules (instead of IP addresses)

Important note!

Apparently now RouterOS supports hostnames in address lists (tested in 6.49.2). So this article is superseeded (just add an hostname in the address list to have it resolved dinamically).

Original post

Althrough the IP layer doesn't carry the hostname used to send that packet (in fact, may not exists, because DNS is higher in ISO/OSI stack), some firewall allows DNS hostnames in "Source" or "Destination" fields. It might be useful in many situations: a fast TTL (for example, NTP Pool project hostnames, "pool.ntp.org"), a dynamic IP address associated to the hostname, a round-robin record with many IPs that might change often (CDN, load balancers, etc). So, how these firewalls can discriminate if there is no such info at IP layer?

A very ugly hack that works

In fact, they don't. Instead, they resolve each hostname at regular interval (smarter firewall uses TTL DNS field) and use the result (IP addresses of course) to build the firewall rule. A very ugly hack.

If you wonder if there is any other solution for these requirements, the answer is no, AFAIK.

Doing this on MikroTik routers

MikroTik RouterOS firewall is an iptables-based firewall, so there is no embedded support to this trick (it might be supported on a fully-fledged OS with some iptables module, but this is not the case).

RouterOS has a "scripting language" to achieve some automations. With this simple script, we exploit the embedded resolver and cache to popolate firewall address-lists.

:local hosts {"it.pool.ntp.org"; "time.nist.gov"; "0.debian.pool.ntp.org"; "1.debian.pool.ntp.org"}
:foreach k,v in=$hosts do={
  :log info "Doing $v"
  :local listname $v
  :resolve $v
  :local iscname [/ip dns cache all find where name=$v and type="CNAME"]
  :if ($iscname != "") do={
    :local newname [/ip dns cache all get $iscname data]
    :log info "$v is CNAME to $newname"
    :set v $newname
  }
  :resolve $v
  /ip firewall address-list remove [/ip firewall address-list find where list=$listname]
  :foreach i in=[/ip dns cache all find where name=$v and type="A"] do={
    :local ipaddr [/ip dns cache all get $i data]
    /ip firewall address-list add list=$listname address=$ipaddr comment=$v
    :log info "IP address: $ipaddr"
  }
  /ipv6 firewall address-list remove [/ipv6 firewall address-list find where list=$listname]
  :foreach i in=[/ip dns cache all find where name=$v and type="AAAA"] do={
    :local ipaddr [/ip dns cache all get $i data]
    /ipv6 firewall address-list add list=$listname address=$ipaddr comment=$v
    :log info "IPv6 address: $ipaddr"
  }
}
:log info "end"

Let's explain:

Many other RouterOS scripts exists for that, but every script has some flaws: the main one is that everyone use ":resolve" to get IP address (but ":resolve" only returns one ip address, even if the hostname has multiple IPs). This script instead is capable to get all IPs (4 or 6).

How to use

Copy this script in RouterOS system scripts, modify the first line (for hostnames to be resolved) and schedule this script to run at regular intervals. After the first execution you'll have the lists populated, and then you can modify firewall rules.

Please mind that ":resolve" on RouterOS throws uncatchable errors: if the hostname doesn't exists, this script will stop at some point, leaving some items not updated.