Hairpin NAT: Is there a simple solution?

What command do I issue to enable hairpin NAT?

It was a challenge just to discover that the name for this behaviour is called hairpin NAT. For future Googlers, hairpin NAT describes the super conventional behavior that when you access your WAN IP address from your LAN, traffic that would get forwarded to another computer on your network (e.g. port forwarding) is modified to appear to come from your router instead, in order to make such traffic work correctly inside your LAN. This is the default behavior for pretty much everything but Microtik.

I use the Microtik extremely conventionally. I use UPnP to configure inbound ports, my WAN IP is dynamic - this is representative of the average US Internet user.

When I visit https://wiki.mikrotik.com/wiki/Hairpin_NAT I see configurations commands that correspond to a very specific kind of configuration.

I would like a general, one time, end-all-be-all way to enable hairpin NAT. No knowledge of my specific internal or external networking required. I am using as close to stock MicroTik configuration as possible.

By comparison, enabling internal DNS names from the hostnames DHCP clients advertise was… not that challenging. I copied and pasted a script that runs every 5 seconds or whatever. This is acceptable! I would prefer a correct and never-needs-to-be-touched-again solution to one that requires me to know which ports I have forwarded where or what my external IP address is.

dd-wrt uses iptables and must have rules that implement this. So what are the rules?

Once it is authored here, I will happily edit the MicroTik Wiki to contain the commands! Personally, if I was a developer, Hairpin NAT is such a fundamental and basic feature / expectation of router behavior that I would add it to Quick Set.

To funny, and by the way, its not a limitation on the MT, its up to the user as per many other functions to program that into the router.
If you want a consumer router…

As for hairpin NAT, it is only required when the user is on the same subnet of the Server that one is trying to reach via WANIP.
Being a simple homeowner I have never understood this approach as I always have used the lanip directly.

In any case, its quite simple one adds another source nat rule as a top srcnat rule in the following format
add chain=src-nat action=masquerade src-address={subnet of server} dst-address={subnet of server}

If you have a static/fixed WANIP, then no change is required to the dst nat rule which will have in it (dst-address=WANIP)
For dynamic WANIPs the dst nat rule usually has something like in-interface-list=WAN, which we replace with dst-address-list=external_wan
where externel_wan is a firewall address list entry with an address=DDNSname ***

*** Enable IP Cloud DDNS, copy DDNS name into the address above, done!

If you just have one web server and you have a DNS server on your router, you can add a static DNS entry to it.
Example you server is www.cnn.com and internal IP is 192.168.88.10, then just add a DNS with that informatin.
User on outside will use the public IP and user on inside will use the inside IP.

DNS have som + and -, same as Hairpin NAT has.

If you have public address directly on router, you can skip DDNS and use this as DHCP lease script:

:if ($bound=1) do={
  /ip firewall address-list set [/ip firewall address-list find where comment="wan1ip"] address=$"lease-address" disabled=no
} else={
  /ip firewall address-list set [/ip firewall address-list find where comment="wan1ip"] disabled=yes
}

It assumes that you have address list item like this:

/ip firewall address-list
add comment=wan1ip disabled=yes list=external_wan

The advantage over relying on DDNS is that updates are instant. DDNS method is useful when there’s NAT 1:1 and router itself doesn’t have public address.

I don’t understand what you did at all. Surprise LOL.

What in plain words does the script do, and the firewall address list item disabled is also confusing.

(I swear you spend all your free time coming up with ways to give me a brain bleed)

It’s an alternative to your:

/ip firewall address-list
add list=external_wan address=<DDNS hostname>

Script goes to:

/ip dhcp-client
add interface=<WAN> <other options> script="<the script I posted>"

You can skip the else branch if you want, it happens when the lease is lost and in that case you don’t have internet access anyway (unless you have another WAN). And when it comes back, the address in list will be updated immediatelly.

(And no, I don’t :smiley:)

I understand my config line, I dont understand what the script is doing.
In plain words… each line.

I’ll try to translate it to NovaScotiaish for you:

1|
2| :if ($bound=1) do={
3|   /ip firewall address-list set [/ip firewall address-list find where comment="wan1ip"] address=$"lease-address" disabled=no
4|  } else={
5|   /ip firewall address-list set [/ip firewall address-list find where comment="wan1ip"] disabled=yes
6| }



  1. brewery’s cart enters the yard
  2. did it bring a full cask?
  3. it did actually … so find that cask in the cellar tagged with “local ale” and pump contents of the new one into the existing. And open the valve (doesn’t matter if it was open already) on the pipe towards bar
  4. hummm, what to do if there’s no full cask on the cart?
  5. drats, close the valve on the old cask in the cellar (the one tagged with “local ale”).
  6. go back behind the bar, observe cart leaving the yard

Wow, what an explanation! I couldn’t write better myself. :smiley:

Now we are getting somewhere, student MKX.
We have tapped into your creative side of the force!!
(and such modesty sob!! truly touching)

anav (yoda) MTUNA.

Well every router I have used has a public IP address so it is not clear to me if you mean static or dynamic (okay I will assume dynamic).
What do leases have to do with dstnat rules and more specifically the replacement of dst-address=, dst-address-list=, OR in-interface-list=WAN.

Furthermore if it directly replaces DDNS then the script seems to SAY okay the router is bound (assuming by ISP wanip connection) check for an address list entry with specific text and it finds it, then stick a value of leased address (that is bounded) there (okay I will buy the lease given by the IP to the router). So that may work. What I dont understand, is the second case, are you saying if I cannot find
a. the address list entry OR
b. cannot find a lease by the ISP (there is no connection to the ISP)

then dont stick the non-existing lease address/IP in the firewall address list???

If so what is the use of that, why wouldnt there be a lease? So you disable the script but left the op without an ISP connection.
How thoughtful.
The script should say
if not bound SEND EMAIL as a minimum to warn the OP that his.her internet is down :stuck_out_tongue_winking_eye:

PS where do you stick this script under system scripts??

If you have static public address, you can simply use dst-addres= and not worry about anything. Lease script is for dynamic public address, but it must be on router (no NAT 1:1). And since it’s DHCP lease script, guess where it goes (you can scroll a little back, I did write it; same for ‘else’ branch).

And sending mail… I don’t know how often your internet goes down, mine maybe once or twice a year. Every single time I think “ok, so I can’t do the thing I wanted to do, no problem, I’ll just check mail or something instead”. And next thought is “damn it!”, every single time too, guess why.

Sob, on the grounds that I might incriminate my own failings, I refuse to state why an email from the router would not work if there was not DHCP client lease LOL.
Also Yes, I see now you have already answered the questions. Unfortunately I already have a script in my dhcp client to take the gateway that is bound and stick in my route rule.
add default-route-distance=255 disabled=no interface=vlanbell
script="":if (
\$bound=1) do={ /ip route set [find commen\\r
\nt=\"BellFibre\"] gateway=(\$\"gateway-address\") disabled=no;
:log warning\\r
\n\
(\"New ISP1 gateway: \".(\$\"gateway-address\")) }""
use-peer-dns=no use-peer-ntp=no_

Would it be possible to add another script without interference either way. The one you suggested does not seem to conflict?
Just for arguments sake as I am sane and rational and only use lanip to access a server when behind the router ;-PPPP

yes

no.

To clarify: the script associated to the dhcp client is run every time the lease status and/or contents changes, no matter how (address acquired, address not renewed, address changed, probably also e.g. address the same like last time but gateway/route list changed). To determine the type of change and possibly take an appropriate action, the script has to check the values in available variables.

So you have to augment your existing dhcp script with additional actions, or you may create several pinpointed scripts under /system script and let the dhcp script just invoke them. Rule of thumb - what fits to a single screen may stay like that, what doesn’t cannot be seen without scrolling anyway so better to split it into blocks fitting to a single screen each. Specially for Mikrotik and command line, editing of a dhcp script lacks the colored syntax check available when editing scripts at other places, so it is a bit painful to debug, which is an additional motivation to keep dhcp scripts as simple as possible and invoke external ones. I suppose that the DHCP-specific variables are only available to the dhcp script itself, but I have never tried that.

As for sending an e-mail when internet fails, not everyone has the luxury of having multiple ISPs active at his location and for prices which allow to pay for two uplinks.

Well the second script really does not affect the dhcp client at all from what I gather.
It basically states, hey if this link is bound, then put the IP of the lease into the address list (in firewall address lists).
If its not bound then dont.

How would this interfere with the DHCP script that is already there???

Are you saying, its better to put that script in the ‘normal’ script area?
If so, can it read the dhcp client info and do all the same things from there?
Or are you saying incorporate the second script into the first script.

I think what your are intimating, is that if I put the second one in the normal script area, then I should modify the existing DHCP client list,
to (AN action), such that everytime the ISP changes something, and a new IP and gateway are provided, then go to another (invoke another) script to update the firewall list.
Just not sure how you invoke a script from a script, problem 1, and if you can invoke a script not in the same area, problem 2.
Problem 3 is basically I am scriptophobic :wink:

You mean Terminal, the one and only place that has it? If your RouterOS has it elsewhere, I want it too! Or perhaps you meant other systems? In any case, colors would be cherry on top, pleasant, but I can easily live without that. Just good old “syntax error at line X”, as my first 8-bit computer could do in last century, would be huge improvement. Current “silent death” mode is really bad. And hey, after all, it is another century, so I want “syntax error at line X, column Y”. :slight_smile:

@anav: In this case it’s the simplest programming, if you have two simple scripts like these, you can put one after another:

:if ($bound=1) do={
  :log info ("(script 1) Address acquired: ".$"lease-address")
} else={
  :log info "(script 1) Address lost"
}
:if ($bound=1) do={
  :log info ("(script 2) Address acquired: ".$"lease-address")
} else={
  :log info "(script 2) Address lost"
}

Or combine them:

:if ($bound=1) do={
  :log info ("(script 1) Address acquired: ".$"lease-address")
  :log info ("(script 2) Address acquired: ".$"lease-address")
} else={
  :log info "(script 1) Address lost"
  :log info "(script 2) Address lost"
}

Both will work. I’m not sure about external scripts and passing variables to them either. As the whole thing works now (see above), it’s not fun to try and explore.

So to get this back on topic, which is sort of illustrative of how needlessly complicated this problem is…

What are the commands to enable hairpin NAT for any and all future possible conventional port forwarding configurations (including those specified by UPnP) for a dynamically assigned IP address and an otherwise conventionally configured router?

I see a very close answer near the top, but I’m still not sure, because without a clear command it’s hard for me to test. I really appreciate the help. For users with services in their internal network, especially using hostname-based modern URL routing like I do, this will be the best script you’ll author today!

I have 3 services running on an internal server (192.168.1.254) which are also available from WAN.
The port numbers are 443, 8051 and 8888
I can reach them from my LAN with that link (example):
http://<my.domain>:8888

These are the Hairpin NAt entrys:

/ip firewall nat
add action=masquerade chain=srcnat comment="Hairpin Masq" dst-address=\
    192.168.1.0/24 dst-port=8888,8051,443 protocol=tcp src-address=\
    192.168.1.0/24
add action=dst-nat chain=dstnat comment="Hairpin 8888" dst-address-list=\
    external-ip dst-address-type=local dst-port=8888 in-interface=bridge1 \
    protocol=tcp to-addresses=192.168.1.254 to-ports=8888
add action=dst-nat chain=dstnat comment="Hairpin 8051" dst-address-list=\
    external-ip dst-address-type=local dst-port=8051 in-interface=bridge1 \
    protocol=tcp to-addresses=192.168.1.254 to-ports=8051
add action=dst-nat chain=dstnat comment="Hairpin 443" dst-address-list=\
    external-ip dst-address-type=local dst-port=443 in-interface=bridge1 \
    protocol=tcp to-addresses=192.168.1.254 to-ports=443

To get my external, dynamic IP i use this script in DHCP client for my WAN port:

:if ($bound=1) do={
  /ip firewall address-list set [/ip firewall address-list find where comment="wan1ip"] address=$"lease-address" disabled=no
} else={
  /ip firewall address-list set [/ip firewall address-list find where comment="wan1ip"] disabled=yes
}

Works perfect for me even on dynamic WAN ip´s

-faxxe

Required srcnat rule can be just one and it covers all ports (192.168.88.0/24 is LAN subnet):

/ip firewall nat
add chain=srcnat src-address=192.168.88.0/24 dst-address=192.168.88.0/24 action=masquerade

Add it once and you won’t have to touch it again. Main problem is how to create dstnat rules:

/ip firewall nat
add chain=dstnat <????> protocol=tcp dst-port=1234 to-addresses=192.168.88.x to-ports=2345
<????> is where you choose how to match destination. You can't use the popular in-interface= or in-interface-list=, because it wouldn't match connections from LAN. Basic choices are:

a) dst-address= for static address or scripted updates for dynamic
b) dst-address-list= for static adddress, scripted updates for dynamic, or automatic resolution using DDNS hostname
c) dst-address-type=local for any address on router, with additional dst-address(-list)=! when you want to dstnat some port to internal server, but also use it on router (e.g. 80 for WebFig)

That’s when public address is directly on router. If it’s elsewhere (router is behind NAT), you need to look for two different destinations, public address for connections from inside and another for connections from outside. That can be either router’s address on WAN interface, or in-interface(-list)= would work too, but with that you’d need two separate rules. If you use dst-address-list containing both addresses, it can be done using one rule.

So yes, it may seem kind of complicated, but what can you do, NAT sucks and most of the world ignores the real solution (which is IPv6) for last twenty years.

This is really close.

Is there a way where I do not have to create an additional rule for every port? I already have UPnP port forwarding. I would like to somehow make those UPnP forwarding / NAT rules just work with hairpin NAT.