Well, configuring hairpin NAT is easier than I thought, but much harder than it should have been. Long story short, the MikroTik way doesn’t work. At least, the MikroTik way didn’t work for me, and it didn’t work for a lot of other people on the internet either.
The way that DID work for me was written by someone who claims not to know anything about networking, not that they wanted to make the rest of us feel bad… I'm sure. That post is (possibly was) here.
The author is a fan of address lists and I must profess that I am too. So, first off we create the address list for the internal ranges.
MikroTik address lists
1/ip/firewall/address-list add address=198.51.100.0/24 list=Internal2# If you have more internal ranges, add them3/ip/firewall/address-list add address=203.0.113.0/24 list=Internal
Address lists are a brilliant way of making your configurations more adaptable and easier to maintain and update. For instance, in the future if I add another internal range I won’t need to change all the rules to also encompass this new subnet, I can just add it to the address list, and it is already sorted, magic.
So, we have the internal range(s), we need to get the external/public IP. I think I have a static public IP but I can’t be sure, it might just not change much. If you don’t have a static IP then you will need a way for your MikroTik to update the rules with whatever your active Public IP is. Again, we can do this with an address list. If we add a hostname to an address list then the MikroTik will resolve the hostname and add that IP to the address list dynamically. The hostname to use is DDNS name from the router itself, because you have that running, right?
1[admin@MikroTik] > /ip/cloud print2 ddns-enabled: yes3 ddns-update-interval: none4 update-time: yes5 public-address: 203.0.113.1226 public-address-ipv6: 2a02:cafe:aced:dead:d601:c3ff:fe5c:58487 dns-name: l4g3dwwwazq3.sn.mynetname.net8 status: updated9 back-to-home-vpn: revoked-and-disabled
Now that we have the hostname that is tied to our external IP l4g3dwwwazq3.sn.mynetname.net
we can put that into an address list, as follows.
1/ip/firewall/address-list add address=l4g3dwwwazq3.sn.mynetname.net list=WAN
That will then add our external IP 203.0.113.122 to the WAN address list. There is now an Internal and a WAN address list that are available to use in the firewall configurations that are to come.
The first firewall rule is a prerouting rule. It marks the connections with a connection-mark. The connection-mark can then be used for the additional firewall rules
1/ip/firewall/mangle add action=mark-connection chain=prerouting comment="Mark connections for hairpin NAT" dst-address-list=WAN new-connection-mark=hairpinning passthrough=yes src-address-list=Internal
Setting up the hairpin NAT

The next step is to perform the NAT on the connections that have the hairpinning connection-mark.
1/ip/firewall/nat add action=masquerade chain=srcnat comment="Hairpin NAT" connection-mark=hairpinning place-before=0
The next part is the port-forwarding and honestly this is the part that caught me out for a while. I already had port-forwarding set up, and I was using interface-lists for those firewall rules. Doing it that way was fine when all the requests were coming in from the internet. However, when the requests are coming from the internal addresses and the Internal address list and therefore an internal interface, the criteria for the rule is not met. The internal interfaces are not in the external interfaces that the firewall rules were assigned to so they weren’t being used.
Moving from an in-interface/in-interface-list of external to a dst-address-list of WAN, everything started working once the firewall rule was actually being triggered by the traffic that should have been triggering it.
1/ip/firewall/nat add action=dst-nat chain=dstnat comment="Port forward HTTP" dst-address-list=WAN dst-port=80 protocol=tcp to-addresses=198.51.100.101 to-ports=802/ip/firewall/nat add action=dst-nat chain=dstnat comment="Port forward HTTPS" dst-address-list=WAN dst-port=443 protocol=tcp to-addresses=198.51.100.101 to-ports=443
The MikroTik way does work
The day after I had this working I realised that the interface lists that I was using might also have been the reason that the MikroTik way was not working. It looks like that is the case. However, I am going to stick with the method that I got it working with originally. The reason being that the MikroTik method (from what I can tell) requires a specific rule for each destination. The other method performs the src-nat masquerade on any connection from the internal address list that is sent to the WAN IP. I want it to be more of a catch-all than specific rules.
Comments