Hairpin NAT doesn't work

Hi,
I’m trying to set up Hairpin NAT on my MikroTik router, but nothing works. I’ve tried many guides, but none of them worked.

My Hairpin NAT and port forwarding rules:

/ip firewall nat
add action=masquerade chain=srcnat comment=DUPA dst-address=192.168.1.100 out-interface=\
    bridge_lan protocol=tcp src-address=192.168.1.0/24
/ip firewall nat
add action=dst-nat chain=dstnat comment="matrix 8448" dst-address-list=WAN dst-port=8448 \
    in-interface-list=WAN port="" protocol=tcp to-addresses=192.168.1.100 to-ports=8448

When I enter my public IP address in browser, webfig opens.

My device: hAP ax2 on RouterOS 7.17

/export file=anynameyouwish (minus router serial device, any public WANIP information, keys).

After you post the complete config, happy to comment.

# 2025-06-07 14:22:10 by RouterOS 7.17
# software id = XAII-WW51
#
# model = C52iG-5HaxD2HaxD
# serial number = XXXX
/interface bridge
add name=bridge_guest
add name=bridge_lan
/interface ethernet
set [ find default-name=ether2 ] comment=AP
set [ find default-name=ether5 ] comment=switch
/interface wireguard
add listen-port=1234 mtu=1420 name=wireguard
/interface vlan
add interface=ether1 name=vlan35 vlan-id=35
/interface pppoe-client
add add-default-route=yes disabled=no interface=vlan35 name=orange-ppp user=xxxx@neostrada.pl
add add-default-route=yes disabled=no interface=vlan35 name=orange-ppp-v6 use-peer-dns=yes user=xxxx@neostrada.pl/ipv6
/interface list
add name=WAN
add name=LAN
/interface wifi configuration
add country=Panama disabled=no name=dupa security.authentication-types=wpa2-psk .wps=disable ssid=XXXX
add country=Panama disabled=no name=hotspot security.authentication-types=wpa-psk,wpa2-psk .ft=yes ssid=XXXX
/interface wifi
set [ find default-name=wifi1 ] configuration=hotspot configuration.mode=ap disabled=no security.authentication-types=wpa-psk,wpa2-psk .wps=disable
set [ find default-name=wifi2 ] configuration=hotspot configuration.antenna-gain=0 .mode=ap disabled=no security.authentication-types=wpa-psk,wpa2-psk .wps=disable
add configuration=dupa configuration.mode=ap mac-address=XXXX master-interface=wifi1 name=wifi3
add configuration.mode=ap .ssid=test mac-address=XXXX master-interface=wifi2 name=wifi4
/ip pool
add name=dhcp_pool0 ranges=192.168.1.100-192.168.1.254
add name=dhcp_pool1 ranges=192.168.21.2-192.168.21.254
/ip dhcp-server
add address-pool=dhcp_pool0 interface=bridge_lan lease-time=1d name=dhcp1
add address-pool=dhcp_pool1 interface=bridge_guest name=dhcp2
/interface bridge port
add bridge=bridge_lan interface=ether2
add bridge=bridge_lan interface=ether3
add bridge=bridge_lan interface=ether4
add bridge=bridge_lan interface=ether5
add bridge=bridge_guest interface=wifi2
add bridge=bridge_guest interface=wifi1
/interface list member
add interface=orange-ppp list=WAN
add interface=bridge_lan list=LAN
add interface=*C list=LAN
add interface=bridge_guest list=LAN
add interface=wireguard list=LAN
/ip address
add address=192.168.1.1/24 interface=bridge_lan network=192.168.1.0
add address=192.168.100.2/24 interface=ether1 network=192.168.100.0
add address=192.168.11.1/24 interface=wireguard network=192.168.11.0
add address=192.168.21.1/24 interface=bridge_guest network=192.168.21.0
/ip cloud
set ddns-enabled=yes ddns-update-interval=2m
/ip dhcp-server lease
add address=192.168.1.200 client-id=XXXXmac-address=XXXX server=dhcp1
add address=192.168.1.100 client-id=XXXX mac-address=XXXX server=dhcp1
/ip dhcp-server network
add address=192.168.1.0/24 dns-server=192.168.1.1 domain=lan gateway=192.168.1.1
add address=192.168.21.0/24 dns-server=192.168.21.1 gateway=192.168.21.1
/ip dns
set allow-remote-requests=yes servers=8.8.8.8,8.8.4.4
/ip firewall address-list
add address=192.168.1.0/24 list=LAN
add address=homelab.mily.ovh list=WAN
/ip firewall filter
add action=accept chain=input comment=Wireguard dst-port=13231 protocol=udp
add action=accept chain=input connection-state=established,related,untracked
add action=drop chain=input connection-state=invalid
add action=accept chain=input protocol=icmp
add action=drop chain=input in-interface-list=!LAN
add action=fasttrack-connection chain=forward connection-state=established,related hw-offload=yes
add action=accept chain=forward connection-state=established,related,untracked
add action=drop chain=forward connection-state=invalid
add action=drop chain=forward connection-nat-state=!dstnat connection-state=new in-interface-list=WAN
add action=drop chain=forward comment="block lan from hotspot" dst-address=192.168.1.0/24 src-address=192.168.21.0/24
/ip firewall nat
add action=masquerade chain=srcnat comment=DUPA dst-address=192.168.1.100 out-interface=bridge_lan protocol=tcp src-address=192.168.1.0/24
add action=masquerade chain=srcnat comment=NAT out-interface-list=WAN
add action=dst-nat chain=dstnat comment="matrix 8448" dst-address-list=WAN dst-port=8448 in-interface-list=WAN port="" protocol=tcp to-addresses=192.168.1.100 to-ports=8448
/ip route
add dst-address=192.168.11.0/24 gateway=wireguard
add disabled=no dst-address=192.168.1.0/24 gateway=bridge_lan routing-table=main suppress-hw-offload=no
/ip service
set telnet disabled=yes
set ftp address=192.168.1.0/24,192.168.11.0/24
set www address=192.168.1.0/24,192.168.11.0/24
set ssh address=192.168.1.0/24,192.168.11.0/24
set api disabled=yes
set winbox address=192.168.1.0/24,192.168.11.0/24
set api-ssl disabled=yes
/ipv6 address
add from-pool=orange interface=bridge_lan
/ipv6 dhcp-client
add interface=orange-ppp-v6 pool-name=orange pool-prefix-length=56 request=prefix use-peer-dns=no
/ipv6 dhcp-server
add address-pool=orange dhcp-option=custom-dns interface=bridge_lan name=server1 prefix-pool=orange
/ipv6 firewall filter
add action=accept chain=input comment="Allow established/related input" connection-state=established,related disabled=yes
add action=accept chain=forward comment="Allow established/related forward" connection-state=established,related disabled=yes
add action=accept chain=forward comment="Allow WAN access to host on ports 22, 80, 443" disabled=yes dst-address=xxxx dst-port=22,80,443 protocol=tcp
add action=accept chain=forward comment="Allow ICMPv6 ping to host" disabled=yes dst-address=XXXX icmp-options=128:0 protocol=icmpv6
add action=accept chain=forward comment="Allow intra-LAN IPv6 traffic" disabled=yes dst-address=XXXX src-address=XXXX
add action=drop chain=input comment="Drop all other input" disabled=yes
add action=drop chain=forward comment="Drop all other forward" disabled=yes
/system clock
set time-zone-name=Europe/Warsaw
/system identity
set name=router-dom
/system note
set show-at-login=no
  1. I am not familiar with IPV6 so cannot comment on two PPPOE settings.

  2. This shows an error in an interface selection
    add interface**=C* list=LAN

  3. This address should be removed…
    add address=192.168.100.2/24 interface=ether1 network=192.168.100.0

Your WAN connection coming on vlan35 from the ISP over ether1 is handled/terminated by the PPPOE connections!!

  1. To setup firewall rules for port forwarding from exterior and interior modify your forward chain rules, after the good default ones there
    from:
    add action=drop chain=forward connection-nat-state=!dstnat connection-state=new in-interface-list=WAN
    add action=drop chain=forward comment=“block lan from hotspot” dst-address=192.168.1.0/24 src-address=192.168.21.0/24

TO:
add action=accept chain=forward comment=“internet traffic” in-interface-list=LAN out-interface-list=WAN
add action=accept chain=forward comment=“wg to LAN” in-interface=wireguard out-interface-list=LAN
add action=accept chain=forward comment=“port forwarding” connection-nat-state=dstnat
add action=drop chain=forward comment=“drop all else”

The last rule should drop any L3 traffic between bridges, you can test.
However if you want LAN to reach hotspot you would have to add AN ALLOW rule for that traffic before the drop all else rule at the end.

  1. Hairpin NAT rule" - modify, simply a bit:
    add action=masquerade chain=srcnat comment=DUPA dst-address=192.168.1.0/24 src-address=192.168.1.0/24
    add action=masquerade chain=srcnat comment=NAT out-interface-list=WAN

  2. Terrible nomenclature, keep firewall address list name separate from interface names already in use aka WAN
    /ip firewall address-list
    add address=192.168.1.0/24 list=LAN <---- I would change this too, as it conflicts with an existing interface name.
    add address=homelab.mily.ovh list=myWAN

THEN apply to dstnat rule but REMOVE in-interface-list=WAN
/ip firewall nat
add action=dst-nat chain=dstnat comment=“matrix 8448” dst-address-list=myWAN dst-port=8448
protocol=tcp to-addresses=192.168.1.100

Note: if To port same as dst-port, its not required to add.

Ok, thank you. I will try your solution.
192.168.100.2 on ether1 is for ONT management

Hairpin NAT still doesn’t work

Here’s my config, adjust as needed:

/interface list
add name=wan

/interface list member
add interface=ether1_wan list=wan
add interface=6to4_waw1 list=wan
add interface=lte1 list=wan

/ip firewall filter
add action=accept chain=forward comment=hairpin/dst-nat connection-nat-state=dstnat in-interface-list=wan src-address-list=!rfc-1918
add action=accept chain=forward connection-mark=hairpin connection-nat-state=dstnat

/ip firewall mangle
add action=mark-connection chain=prerouting comment="mark packets destined to wan from internal as hairpin" dst-address-list=wan in-interface-list=!wan new-connection-mark=hairpin src-address-type=!local

/ip firewall nat
add action=masquerade chain=srcnat comment="masquarade srcnat" out-interface-list=wan
add action=netmap chain=srcnat comment="masquarade hairpin" connection-mark=hairpin to-addresses=100.64.0.0/10
add action=dst-nat chain=dstnat comment=www dst-address-list=wan dst-port=80,443 protocol=tcp to-addresses=10.11.0.123

/ip firewall address-list
add address=192.168.0.0/16 list=rfc-1918
add address=172.16.0.0/12 list=rfc-1918
add address=10.0.0.0/8 list=rfc-1918
add address=0.0.0.0/8 list=rfc-1918
add address=224.0.0.0/3 list=rfc-1918
add address=127.0.0.0/8 list=rfc-1918
add address=204.152.64.0/23 list=rfc-1918
add address=169.254.0.0/16 list=rfc-1918
add address=255.255.255.255 list=rfc-1918
add address=**.**.**.*** comment="dynamic ether1_wan" list=wan
add address=*.***.***.*** comment="static lte1" list=wan

If your IP is dynamically assigned by DHCP, you can borrow this (slightly modified to remove manual route mangling) script to set address-list entry:

:local name [/interface/get $interface name]

/ip/firewall/address-list/remove [find list=wan comment="dynamic $name"]

:if ($bound = 1) do {
  :if ([:len ($"lease-options"->"6")]) do {
    /ip/firewall/address-list/add list=wan address=$"lease-address" comment="dynamic $name"
  }
}

When using it, I also recommend creating scheduler entry as such:

/system scheduler
add comment="runs one time on boot; cleans stale address-list" name=onboot on-event="# clean stale addresses\r\n/ip/firewall/address-list/remove [find list=wan comment~\"^dynamic .+\"]" policy=read,write,policy,test start-time=startup

OP, post your config with the changes suggested by anav.

As for IPv6, I’m no expert here but the firewall seems a bit breezy (no active drop rules)

Eider, why do you post a config that is nothing but a security phuckup…
Ahh okay, that makes more sense…

Clearly it’s a snippet, not entire configuration? I’ve only included what’s needed for hairpin using netmap. That’s the entire point of textual configuration, isn’t it? We share snippets, and people learn from them and adapt them to their own config. No different than sharing snippets for Junos or IOS. The person already has their own config, I’m not gonna force my own filter rules on them (not that they would even fit considering how complex my entire configuration is in forward chains).

I’m only going to comment on the “hairpin nat” part of things, because that was the question - however I fully agree that securing your network and router are important, and consideration should be given to that.

Hairpin NAT is used to access “port forwarded” services from not only outside the network, but from the inside as well. Both rules have to be adequately configured for things to work.

The “port forwarding rule” has to filter by the dst ip addr and not the input interface, because when the forwarded service is accessed from the inside, then - well - the connection is coming from inside the network and not outside.

There are two options: One in to maintain an address list containing the external address of the router. I use this approach on my own routers, but it usually means scripting.

The other: use dst-address-type=local, which means that the router is addressed on any of its own addresses. (This may be slightly counter-intuitive: type local doesn’t mean “local” in the sense of “on your LAN”, but means the addresses that are “local to the router itself”, that is: the router’s own addresses.)

So the rule:

/ip/firewall/nat/add chain=dstnat action=dst-nat dst-address-type=local protocol=tcp dst-port=8080 to-addresses=192.168.88.10 to-ports=80

(This rule maps port 8080 to an internal server 192.168.88.10, and sends the connection to port 80.)

The other rule is the actual hairpin rule. I think that hairpin nat should generally be regarded as a not elegant, but sometimes very useful hack. Therefore I usually create hairpin rules to only affect traffic that absolutely needs it (and leaves other connections be). This is therefore my haripin rule corresponding to the above rule:

/ip/firewall/nat/add chain=srcnat action=masquerade src-address=192.168.88.0/24 dst-address=192.168.88.10 protocol=tcp dst-port=80

Note that when the hairpin (srcnat) rule is executed, the port forwarding (dstnat) has already been done, and the packet now matches the rule in its translated state with regard to its dst address and dst port, as shown in my example. It’s important to include the src-address criterion in the hairpin rule, otherwise all access (even from external sources) will appear as coming from the router’s local address.

Happy hacking!

Resurrection taking place...

Why do you prefer adding your WAN ip to an address list instead of using "dst-address-type=local"?

"slightly counter-intuitive?" Nah, couldn't be!

Gosh dang it -- I did not understand "local" in this context this until stumbling on this thread!

Why is there no limitations on the source? Or other limitations? Do we want all traffic destined for port=8080 to be forwarded to 192.168.88.10:80?

Would you kindly use your elegant communication skills to explain exactly what this command does and why?

Struggling a little here....

hairpin (i.e., chain=srcnat rule) is executed first (i.e., before the port forwarding (chain=dstnat) rule)?

Could you please translate "matches the rule in its translated state?"

Thank you!

  1. ADDING SOURCE-NAT RULE (HAIRPIN)

To fix the scenario where the LAN users and the Server are on the same subnet, all that is required is the following generic source NAT rule, often called the HAIRPIN NAT Rule placed as the first source NAT rule (although I have been told order here is not important). This required source NAT rule is independent of the type of WANIP (static/dynamic).

The key addition to the NAT rules is the addition of a source-nat rule placed before the default src-nat rule (substitute with your server subnet).
add chain=srcnat action=masquerade dst-address=192.168.88.0/24 src-address=192.168.88.0/24

Courtesy of Sob, (the problem):
"-[i] user client 192.168.88.5 wants to connect to www.myserver.net, resolves hostname, gets 47.123.12.89 and sends initial packet to it

  • client doesn't have any idea where 47.123.12.89 is, as far as it knows, it can be on the other side of planet
  • dstnat rule changes packet's destination address to 192.168.88.68 and sends it to server
  • source address is not changed, it's still 192.168.88.5 <- the problem
  • server gets the packet and sends response directly to 192.168.88.5, because it's in same subnet[/i]!!
    - client throws it away, because it doesn't expect any response from 192.168.88.68
    the client is expecting a response from 47.xx

WHY this additional SRC NAT rule works: When the client sends the outgoing request now two things happen, the first thing is as before the destination NAT rule will change the destination from that resolved to the WANIP 47.xx to that of the Server .68, and now additionally, the outgoing traffic also gets masqueraded gets 'natted' to the subnet gateway address. Therefore the traffic is now headed to the Server with source IP address of the gateway of the subnet (vice the client). Then when the server replies to the traffic it sends the traffic back to the router as what it sees now as the source (originator of the traffic) is the IP 192.168.88.1. The router tracking the packets then applies the dst-nat rules and masquerade rules recognizing the returned traffic and thus un dst-nats the traffic from dst of 192.168.88.86 back to original dest of 47.xx and un-source nats the traffic from 192.168.88.1 to the original 192.168.88.5 and the packet arrives at the client and is accepted.

The explanation diagrams found here may be helpful(courtesy of Greg) : http://gregsowell.com/?p=4242

Finally this explanation on hairpin nat from MKX is invaluable!!
Standard SRC-NAT is masquerading source address and standard DST-NAT is masquerading destination address. And hairpin NAT is masquerading both addresses, one to each end (client doesn't use the correct dst-address and server doesn't see correct src-address).

Usual hair-pin NAT (in typical port forwarding) makes sure that the LAN client can talk to LAN server via some foreign (to both client and server) IP address, in the case of masquerading (redirecting) WANIPs, then the case becomes the WAN client talking to LAN server via some foreign (to both client and server) IP address (and that communication needs to pass router in both directions). Remember that any of the router's own IP addresses are treated equally regardless of ingress interface of a particular packet

  • e.g packet targeting router's WAN IP address, ingressing through LAN interface is treated the same way as packet targeting same IP address ingressing through WAN interface.

Thanks anav!

Do we need 2 rules in order to achieve hairpin nat: A DSTNAT and a SRCNAT rule.

Or, just 1 rule:

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

This from mkx is indeed very clear:

Standard SRC-NAT is masquerading source address and standard DST-NAT is masquerading destination address. And hairpin NAT is masquerading both addresses, one to each end (client doesn't use the correct dst-address and server doesn't see correct src-address).

I think this means that the "1 rule" above masquerades both the destination address (i.e., changes it from the public IP to the server's internal/private IP) as well as the source address (from the internal IP to the public IP) so that (1) routing of frames is done correctly and (2) both the client and server see the expected source and destination IP address on the frames.

If my understanding is correct, then am I misunderstand lurker888's post to state that 2 rules are necessary: A port forwarding (dstnat to-port) rule and a hairpin (srcnat masquerate) rule? Or was he addressing the specific needs of the original poster (and not a generic hairpin need)?

In addition, I cannot understand the paragraph that starts "Usual hair-pin NAT (in typical port forwarding) makes sure that the LAN client can talk to LAN server via some foreign...."

Because there are cases where you don't want the port forwarding to work on the router's local IP address. For example, you want to port forward TCP 443 on your WAN address to some web server / reverse proxy running on your LAN, but still want to access https://192.168.88.1/ to manage the router with WebFig?


Because if you want to allow access to the services with the forwarded ports to clients from outside (the internet, like someone using mobile phone on LTE/5G), which means the source IP address can be any IPv4 address, and the only realistic source address filtering is src-address=0.0.0.0/0, which means you can remove that restriction anyway.


  • When you on a PC in the local LAN with the IP address 192.168.88.15 connects to router_(WAN)_ip:8080 using TCP, let's assume the router's IP address used is 123.45.67.89, the packet will initially have src-address=192.168.88.15, dst-address=123.45.67.89, dst-port=8080, protocol=tcp. And let's says the source port is src-port=58225.

  • This DSTNAT rule will be applied on the connection:

    /ip/firewall/nat/add chain=dstnat action=dst-nat dst-address-type=local \
       protocol=tcp dst-port=8080 to-addresses=192.168.88.10 to-ports=80
    

    This causes the packet to have dst-address=192.168.88.10 and dst-port=80, while retaining the source address src-address=192.168.88.15 and src-port=58225.

  • Before the packet leaves the router (to be sent to 192.168.88.10:80), the SRCNAT masquerade rule:

    /ip/firewall/nat/add chain=srcnat action=masquerade \
       src-address=192.168.88.0/24 dst-address=192.168.88.10 protocol=tcp dst-port=80
    

    which is the Hairpin-NAT rule, will act on this packet, because dst-address, dst-port, protocol, and src-address all match with the packet's properties. This masquerade rule will cause the source address of the packet to be changed, from src-address=192.168.88.15 to src-address=192.168.88.1, with 192.168.88.1 being the IP address of the router on that bridge interface. Depending on whether the port is unavailable, it might also change the source port to src-port=24852.

    This causes the packet to arrive at the 192.168.88.10 destination server bearing the source IP address 192.168.88.1 and src-port=24852. When the web server response to this packet, it will send response packets with dst-address=192.168.88.1 with dst-port=24852 (and src-address=192.168.88.10, src-port=80).

  • The router is the destination of this packet. It will receive this packet, will look up in the connection tracking table, and see that the packet belongs to a connection that has both DSTNAT and SRCNAT (in the IP -> Firewall -> Connections table it has both the s and d flags). The router will reverse the DST & SRCNAT transformation, the source of the response packet will be changed to src-address=123.45.67.89 and src-port=8080, while the destination address is changed to 192.168.88.15, which is your PC, and change dst-port=58225, the originating port.

    This packet is sent to the PC on 192.168.88.15:58225 and the PC sees it as a response from an expected source 123.45.67.89:8080, and consumes it.

Now why was Hairpin-NAT (the SRCNAT masquerade rule) needed?

  • Without that rule, the web server at 192.168.88.10:80 will receive a packet with the origin 192.168.88.15:58225. The web server has no problem with this, will process it and send a response. But now the response packet has dst-address=192.168.88.15 and dst-port=58225.

  • Because 192.168.88.15 is in the same subnet 192.168.88.0/24 as the web server, this destination is considered on-link. The web server will not use the gateway (the router) to send the response packet, but will try to send directly on the ethernet layer, by using ARP to ask for the MAC address of 192.168.88.15, then send the ethernet frame directly to the PC at 192.168.88.15.

  • The PC will receive a packet with dst-address=192.168.88.15, dst-port=58225 BUT with the source being src-address=192.168.88.10 and src-port=80! This is a packet that the PC did not expect at all, because it previously sent something to 123.45.67.89:8080, and not 192.168.88.10:80. The PC cannot assign this packet to any previous connection. And its firewall will probably drop this packet. Which means the PC gets no response when sending request to http://123.45.67.89:8080/.

That's why the Hairpin-NAT rule is needed.


We need two rules. As I already wrote in my post you nicely quoted for me, hair-pin NAT is required in situations when both SRC address and DST address need to be rewritten for connections to work. And single NAT rule only rewrites one address: either SRC address or DST address depending on which chain processes packets (srcnat or dstnat). Type of action doesn't affect this, it defines how the address in question is replaced in when performing SRC NAT (masquerade takes router's IP address on egress and applies it as SRC address to packets being src-NATed while src-nat takes IP address specified with to-addresses property to the same setting and applies it to packets being SRC NAT-ed).

Keeping it simple, the dstnat rule is like any consumer routers port forwarding rule. One needs to tell the router where to send traffic bound for port XX. So I disagree in the wording by others. One needs only one EXTRA rule from normal, for hairpin nat and that is the hairpin nat rule.

The hairpin nat rule is required whenever, the following conditions are true.....
a. one is accessing a server within the same subnet
b. one is using the WANIP of the router (either direct IP address or domain/url name )

The two simple solutions are:
(i) use the LANIP of the server vice WANIP for users in the same subnet
( however folks are addicted to domain/url names )
(ii) place the server or users on a different subnet.

There is usually stuff that I want to do with the external address that's dynamically assigned so I always add an on-lease or on-connect script to the appropriate client. One of the things it does is populate an address list with the address. It's useful when filtering for all sorts of purposes. Using dst-address-type=local is fine.

:slight_smile:

The latter is exactly the intention and meaning of the rule. Usually port forwarding means that anyone external is allowed to access the resource. If you're doing hairpin nat, that basically means everyone external+internal - so there's no point in a restriction. Of course if you wish to restrict who has access to the given resource, this is a valid place to enforce this restriction. (I only wrote it this way because in my experience that's not the common case. It's also totally valid to enforce further restrictions in the forward chain - although if I want to do so I usually do it in the dstnat rule.)

This is the actual haripin nat rule.

  • the matchers identify the dst-nat-ed traffic (more on this later)
  • srcnat (and masuqerade) means that we're going to manipulate the source address of the packet
  • masquerade means that the source address we substitute is the address the router would use for reaching the given destination address (in the example, 192.168.88.1)

I would presume you read my comment a bit hastily. In fact, I'm saying the opposite: dst-nat is executed first, and the src-nat rule only later. (You may want to refer to jaclaz's excellent packet flow diagram.)

The second part of my description tries to helpfully draw attention to the fact that when the srcnat rules are consulted, the packet has already been translated by the dstnat rule, and therefore to correctly match it, we must construct a rule that matches not the original packet, but the packet in its already translated (altered) form.

You seem to be a bit confused about which rule is the "port forwarding" and which is the "hairpin" rule. You are correct in being so. There is widespread confusion on these points: other manufacturers routinely place an all-encompassing hairpin rule in their rulesets implicitly, others translate the source addresses of all port forwarded packets to their own, etc. This leads to all sorts of weirdness, like a web server that is being port forwarded to being under the impression that all its connections come from the router itself. When one peeks behind the curtain a bit, these practices turn out to be easy to use but ultimately detrimental.

These are not strictly defined terms, and I cannot claim to be the ultimate authority on their meaning or usage - what I try to do is use it consistently, and in my terminology the dstnat rule is the "port forwarding rule" (that makes the resource externally accessible) and the srcnat the "hairpin rule" (that enables access from the same internal subnet as well.)

In my previous life with Zyxel routers that had a checkbox entitled loopback (that is like hairpin). It wasnt till I started using MT that I actually learned about packet flow and what goes on behind the scenes ( at least less superficially). That is why I usually modify the firewall chain to be:

add chain=forward action=accept connection-nat-state=dstnat 

so that the config allows both external ( the usual scenario ) and internal ( which the default rule blocks ).

On the zyxel one did use firewall rules to allow block separate port forwardings, but I find MTs method more efficient.

Wow, thank you all, as always!

At further risk of further exposing my ignorance, I'm going to attempt to communicate, in a simplified way, my understanding.

When a LAN client (e.g., 192.168.88.200) wants to reach (i.e., connect or establish a connection to) a service at a specific port (e.g., 8080) on a local server (e.g., 192.168.88.10) by using the WAN ip address (or resolved domain name), two (2) NAT rules are required:

  1. A DST NAT rule to forward frames destined for the specific port that arrives on the WAN port of the router to the specific IP address [and port] of the local server; An example of the rule would be:
/ip/firewall/nat/add chain=dstnat action=dst-nat dst-address-type=local \
protocol=tcp dst-port=8080 to-addresses=192.168.88.10 to-ports=8080

[Alternative src and dst ports can be mapped also, such as "dst-port:1234" and "to-ports=80"]

[Another option is to replace "dst-address-type=local" with "dst-address-list=" that contains the WAN ip]

  1. A SRC NAT rule to change the source IP of all frames from the LAN network to the WAN port so that when a local server sees the frames those frames have the local address and port.
/ip/firewall/nat/add chain=srcnat action=masquerade \
   src-address=192.168.88.0/24 dst-address=192.168.88.0/24

[This rule can be limited or more specific by adding specific "dst-address=" and/or "dst-port="]

How'd I do?