Hairpin NAT - I *think* I did it right!

I just made some adjustments to my setup - that not only work but they feel right. Please tell me if this works for you or if there’s a flaw somewhere…

One of the issues with “hairpin NAT” is losing the external IP’s. For my purposes that’s a problem. I’m far less concerned about logging internal clients accessing internal resources via external IP’s - the typical case for this is a mobile phone connected to the office wireless that still has the cached external DNS entry for the mailserver. “Hardwired” internal clients are served via split-horizon DNS so they aren’t a problem. But misbehaved mobiles…

#First, declare all LAN's for future use, also my external IP:
/ip firewall address-list
add address=10.59.97.0/24 list=LANs
add address=10.21.3.0/24 list=LANs
add address=10.255.255.0/24 list=LANs
add address=192.168.0.0/24 list=LANs
add address=192.168.11.0/24 list=LANs
add address=1.2.3.4 list=static-internet
#
# Now mark matching connections/packets
#
/ip firewall mangle
add action=mark-connection chain=forward comment="Classify: LAN/VPN dstnat redirect" connection-nat-state=dstnat connection-state=related,new,untracked dst-address-list=LANs new-connection-mark=DSTNAT-LAN passthrough=yes src-address-list=LANs
add action=mark-packet chain=forward comment="Packet Mark: LAN/VPN dstnat redirect" connection-mark=DSTNAT-LAN new-packet-mark=DSTNAT-LAN passthrough=no
#
# Actual hairpin
#
/ip firewall nat
add action=src-nat chain=srcnat comment=Hairpin dst-address=192.168.0.2 packet-mark=DSTNAT-LAN to-addresses=192.168.0.1
add action=dst-nat chain=dstnat comment=Hairpin dst-address-list=static-internet src-address-list=LANs to-addresses=192.168.0.2

What I think I’m doing here, that I haven’t seen elsewhere, is taking advantage of the available mangle connection-nat-state=dstnat that isn’t available in the nat table. As a result - only traffic that matches the dstnat rule, i.e. internal clients requesting the external IP, is subject to the srcnat rule and have their IP obfuscated.

As a further optimization, if only one external IP and one internal server is involved, do all the matching in mangle (replace the above mark-connection & src-nat rules and leave the rest):

/ip firewall mangle
add action=mark-connection chain=forward comment="Classify: LAN/VPN dstnat redirect" connection-nat-state=dstnat connection-state=related,new,untracked dst-address=192.168.0.2 new-connection-mark=DSTNAT-LAN passthrough=yes src-address-list=LANs
/ip firewall nat
add action=src-nat chain=srcnat comment="Hairpin - matched by Mangle" packet-mark=DSTNAT-LAN to-addresses=192.168.0.1

The idea to make it clean by applying srcnat only to dstnatted connections is good, but the result is not that great:

  • Since each connection can have only one mark, it can easily conflict with some other use. But if you don’t need it for anything else, then it’s not a problem.
  • Mangle rules will unnecessarily have to examine every single packet passing through router, if it matches conditions or not.
  • Whole packet marking is unnecessary, you could match connection marks directly.
  • You’re applying srcnat also between different LANs, which is not required, e.g. client 10.59.97.x accessing server 10.21.3.x doesn’t need it. But I can imagine that in some cases it can be intentional.
  • You have extra dstnat rule only for LAN clients, which again is unnecessary, one common dstnat rule for both internal and external clients is enough.

My suggestion, keep it simple. Have common dstnat rules that will work from anywhere:

/ip firewall nat
add chain=dstnat dst-address=<public address> protocol=<protocol> dst-port=<port> action=dst-nat to-addresses=<internal addres>

Then just add srcnat rules for each subnet:

/ip firewall nat
add chain=srcnat src-address=<LAN subnet 1> dst-address=<LAN subnet 1> action=src-nat to-addresses=<some address>
add chain=srcnat src-address=<LAN subnet 2> dst-address=<LAN subnet 2> action=src-nat to-addresses=<some address>
add chain=srcnat src-address=<LAN subnet 3> dst-address=<LAN subnet 3> action=src-nat to-addresses=<some address>
...

It’s safe and foolproof. Normally, unless you make them, packets from some LAN subnet won’t return back to same LAN subnet, so it almost guarantees that those that do were dstnat/hairpin (the other way would be if you played with routing, but you would know about that).

The only exception, where srcnat would apply to something it shouldn’t, is router’s own communication to connected LAN subnet. I’m not completely sure if it has any effects, when you use router’s internal address in given LAN as new source, but you can avoid it if you add src-address-type=!local.

The to-addresses= can be pretty much anything except something else in same LAN where is both client and server. My favourite is to-addresses=, because it’s distinguishable as hairpinned connection. For this, you’d want src-address-type=!local, to exclude connections from router.

I don’t understand this. How are the mangle rules different from any other rule - of course connections & packets have to be matched?


You’re saying only match the connection - the packet marks aren’t necessary?


The other LANs are VPNs - so it’s possible. My goal is to make so any of my LAN/VPN clients, attempting to reach the external IP, are properly directed to the internal server (and I have a single server).


Actually, my “regular” dstnat rules only specify the extyernal IP - so I had that. But it wasn’t working - adding the extra rule I see the traffic being handled. And the hairpin rule is at the bottom - my standard dstnat rules should be processed and matched first. But they don’t…


But that’s exactly what I don’t want to do - that masks the IP’s of all clients. I only want to srcnat those connections that require it - unless I’m misunderstanding.

There’s definitely some misunderstanding. You need hairpin NAT only when client in some subnet connects to public address and it’s redirected by router to server in same subnet. The problem is that without it, server sees original source address, thinks the connection comes from local neighbour and responds directly to it. And it can’t work, because client doesn’t expect response from server’s internal address. Hairpin NAT is hack that fixes this by changing source address to a different one. Either router’s address in that subnet, or some completely different one. One or the other, the goal is for server to send response to router.

If you have only one server, then you need hairpin NAT only for the subnet it’s in. If the server is 192.168.0.2, then the only extra rule you need is:

/ip firewall nat
add chain=srcnat src-address=192.168.0.0/24 dst-address=192.168.0.0/24 action=src-nat to-addresses=<some address>

None of your other subnets need it (as long as you have correct routing).

Not sure about that for my case. With 192.168.0.0/24 served by the router, and 10.59.97.0/24 provided by my server at 192.168.0.2 running OpenVPN - if a local device connects to Wi-Fi and gets a 192.168.0.x/24 address, then connects to VPN and gets a 10.59.97.x/24 address - what happens when it tries to reach the external IP? What substitutes for thinking in my case:

  1. Client looks for 1.1.1.1 - not listed in routes served by VPN.
  2. Client asks default gateway, provided by Wi-Fi/local router, 192.168.0.1 to reach 1.1.1.1.
  3. dstnat re-directs to 192.168.0.2.
  4. Without srcnat - reply is sent to…

Now I gotta think about this some more. If the client is using the VPN for the default gateway, then the communication with the router, in theory, comes via 192.168.0.2 - so the router would be unaware of the 10.59.97.0/24 addresses and no srcnat needed. If the client uses the Wi-Fi, then the communication would be via 192.168.0.x/24 - which means srcnat is needed - but only on the 192.168.0.0/24 subnet exactly as you said. But…I think I lose LAN communication when VPN is enabled on my phone. It’s been a while since I was in the office with this so I think I need some testing.

The structure of your network is not as clear to me as it’s to you, but that’s expected. Maybe some diagram could help.

But generally, if you have proper routing everywhere, i.e. device in any of your networks is able to reach another device in any other of your networks (or it would be, if there would be no firewalls blocking it), then it’s like I wrote, you need hairpin NAT only when client and server are in same subnet. In all other cases, server sees client’s original address and can send responses there.

There could be problems (and hairpin NAT would solve them) if the routing is incomplete, e.g. if client would have route to server, but server would not have route to client. But that would be a mistake that should be fixed.