Hairpin NAT doesn't work

Yep, you got it. Good write up.

Two remarks.

  1. These NAT rules depend on the filter/forward rule allowing connection-nat-state=dstnat rule being present. This is implicitly there in the default config by exempting these packets from the final drop rule in the chain. Absent this, accept rules have to be manually created, either relying on the nat state or conventionally based on ip/port values.

  2. There is a reason I usually match on the specific dst address and port in the masquerade rule, which is that in certain configurations the rule may affect packets it was not intended to. I would expect that those for whom this would matter are already aware of this.

Hi Lurker as stated previously, and to the OP, no only one extra NAT rule is required!
As per above, one also ensures the default forward chain rule ( which I loathe ), needs to be changed to:

add chain=forward action=accept comment="internet traffic" in-interface-list=LAN  \
out-interface-list=WAN
add chain=forward action=accept comment="port forwarding" connection-nat-state=dstnat  \
disabled=yes { enable or remove if not required }
add chain=forward action=drop comment="drop all else"

To explain -- >One always needs a port forwarding rule ( in MT a dstnat rule ) to accomplish reaching a server. Not two rules.
If there was no need for hairpin then that single usual rule is required. The extra rule, the addition to the "usual situation" is the need for the extra hairpin NAT rule when a local user is attempting to reach a Lan server within the same subnet AND refuses to use other alternative means ( move server or users to diff subnet, use DNS methods)

Simply one needs:

add chain=dstnat action=dst-nat  src-address=subnet  dst-address=subnet

This works in all cases and although I see your point about specificity, I disagree and recommend the above because, in my experience an OP tends to have multiple servers on the subnet and this one rule covers any additions of servers, without the need for additional dst rules. One should focus on using the firewall rules to deny/allow traffic as required.

Now as far as the OPs post, the main focus appears to be the fact that, aside from the above needed EXTRA NAT Rule, the rest of the config is simply on how to deal with a dynamic WANIP situation. This should not be confused or mixed up with Hairpin and occurs regardless, when one has a dynamic WANP and wishes to accomplish port forwarding.

Clearly if the OP has a static fixed WANIP, then the port forwarding rule needs no further work FOR BOTH external users and internal users, done and done.

add chain=dstnat action=dst-nat dst-address=fixedWANIP dst-port=AAA protocol=tcp to=address=192.168.88.Y

So it should be stated that when dealing with port forwarding there are at least TWO distinct different issues:
When confronting users in same subnet as the server, One extra NAT Rule as discussed above,
AND if dynamic WANIP
b. Some modifications to the original port forwarding rule

(i) DYNDNS URL approach (mimic fixed wanip scenario)

add chain=dstnat action=dst-nat  dst-address-list=MYWAN dst-port=AAA protocol=tcp \
to-address=192.168.88.Y

(ii) Using LOCAL addresses ( Forcing User through WAN )

/ip firewall-nat
add chain=dstnat action=dst-nat dst-address-type=local \
dst-address=!192.168.88.1  protocol=tcp dst-port=AAA to-addresses=192.168.88.Y 

In this case we push the user through the WAN side as we deny the use of the local LAN subnet for the traffic. Its kind of a kludge and is problematic if there are multiple subnets because then we have to do something like:

/ip firewall-nat
add chain=dstnat action=dst-nat dst-address-type=local dst-address-list=!AllSubnets  \
protocol=tcp dst-port=AAA to-addresses=192.168.88.Y

OP HAS ERROR in his config as he allows local addresses to hit/include 192.168.88.1 and thus it will not work.

+++++++++++++++++++++++++++++++++

We alluded to moving server or users to different subnet to avoid the 'extra' hairpin nat rule.
Using dns technique also avoids the use of hairpin nat rule.