Hairpin NAT with VLANS not working

Hello again, I managed to progress with my mikrotik journey, and got VLANS working. Below is a quick description of the network setup:

One RB5009. One CAP AX connected to ether2. Ether1 is PPPoE. All the RB5009 ports are VLAN100 access ports, except ether2, which is a trunk carrying all three VLANS - 100, 200 and 300 (VLAN100 is also the management vlan). AP is configured with 3 wifis that tag their traffic with 3 different PVIDs. All in all, it works, that’s thankfully not the problem.

The problem is I have a server in VLAN100. Before adding VLANs, the problem of accessing my server by a domain (so I can get cert verification, don’t wanna use local IP) was solved by the Hairpin NAT rule (two rules: first dst-nat to server IP and port (src port 443 to server port 30022), then masquerade local source address). (Using DNS with local record is not an option, since HTTPS service is not served on port 443, so I need port mapping, must pass through firewall/nat. Could isolate server on it’s own VLAN/subnet, but I don’t like that idea…) This worked before VLANs. I could access the server from my network, by the public IP and domain.

Enter VLANs, this no longer works. At the end is my full config. For some reason, it seems the first dst-nat action is not activating at all. Just doesn’t work. I logged the packets, watched the counter, it DOES match the rule, but the action DOES NOT happen. The dst address is not redirected to server (10.0.0.3:30022)!

Two ways I know this: the srcnat rule bellow does not catch the src address of 10.0.0.3 (matched packets do not increment), meaning the previous rule did not change the dst address. Other way, is I used tcpdump and wireshark on the server and caught absolutely nothing coming it’s way on port 30022 (or any port from relevant src ip, for that matter).

In the config below, I dumbed down (removed out filters out-interface VLAN100 and src ip range 10.0.0.0/24) the srcnat rule to just match dst-address 10.0.0.3, for an easier catch (which still does not happen).

I don’t know how to proceed, the action seemingly just doesn’t work. If I didn’t know better I’d say it’s some kind of a bug, but it’s not. I’m missing some kind of info on how this works, and in this case, why doesn’t it work. If anyone can help, that would be great!

2025-02-14 04:25:44 by RouterOS 7.17.2

software id = HS9E-P0TQ

model = RB5009UG+S+

serial number = snip

/interface bridge
add admin-mac=78:9A:18:C7:91:03 auto-mac=no comment=defconf name=bridge
port-cost-mode=short pvid=100 vlan-filtering=yes
/interface pppoe-client
add add-default-route=yes disabled=no interface=ether1 name=pppoe-1
use-peer-dns=yes user=snip
/interface vlan
add interface=bridge name=VLAN100-HOME vlan-id=100
add interface=bridge name=VLAN200-GUEST vlan-id=200
add interface=bridge name=VLAN300-IOT vlan-id=300
/interface list
add comment=defconf name=WAN
add comment=defconf name=LAN
add comment=“Management port” name=MGMT
add name=ALLVLANS
/interface wifi channel
add band=5ghz-ax disabled=no frequency=5210-5700 name=5GHz_mix
skip-dfs-channels=all width=20/40/80mhz
add band=2ghz-n disabled=no frequency=2412-2467 name=2.4GHz_mix
skip-dfs-channels=all width=20/40mhz
/interface wifi datapath
add bridge=bridge disabled=no name=capdp-router
add disabled=no name=datapath-guests vlan-id=200
add disabled=no name=datapath-home vlan-id=100
add disabled=no name=datapath-iot vlan-id=300
/interface wifi security
add authentication-types=wpa3-psk disabled=no encryption=“” name=
home_secure_wpa3psk
add authentication-types=wpa2-psk disabled=no encryption=“” name=
iot_secure_wpa2psk
add authentication-types=wpa3-psk disabled=no encryption=“” name=
guest_secure_wpa3psk_eap
/interface wifi configuration
add channel=5GHz_mix country=snip datapath=datapath-home disabled=no
manager=local mode=ap name=Home5Config security=home_secure_wpa3psk ssid=
snip
add channel=2.4GHz_mix country=snip disabled=no manager=local mode=ap name=
Home2.4Config-iot security=iot_secure_wpa2psk ssid=
snip
add channel=5GHz_mix country=snip disabled=no manager=local mode=ap name=
Home5Config-guest security=guest_secure_wpa3psk_eap ssid=
snip
/interface wifi

operated by CAP D4:01:C3:F8:18:64%VLAN100-HOME, traffic processing on CAP

add configuration=Home2.4Config-iot disabled=no name=cap-wifi2.4GHz
radio-mac=D4:01:C3:F8:18:67

operated by CAP D4:01:C3:F8:18:64%VLAN100-HOME, traffic processing on CAP

add configuration=Home5Config disabled=no name=cap-wifi5GHz radio-mac=
D4:01:C3:F8:18:66

operated by CAP D4:01:C3:F8:18:64%VLAN100-HOME, traffic processing on CAP

add configuration=Home5Config-guest disabled=no mac-address=D6:01:C3:F8:18:66
master-interface=cap-wifi5GHz name=cap-wifi5GHz-guest
/ip pool
add name=default-dhcp ranges=192.168.88.10-192.168.88.254
add name=home-pool ranges=10.0.0.10-10.0.0.254
add name=guest-pool ranges=10.0.2.10-10.0.2.254
add name=iot-pool ranges=10.0.3.10-10.0.3.254
/ip dhcp-server
add address-pool=home-pool interface=VLAN100-HOME name=home-dhcp
add address-pool=guest-pool interface=VLAN200-GUEST name=dhcp-guest
add address-pool=iot-pool interface=VLAN300-IOT name=dhcp-iot
/interface bridge port
add bridge=bridge comment=defconf interface=ether2 internal-path-cost=10
path-cost=10 pvid=100
add bridge=bridge comment=defconf interface=ether3 internal-path-cost=10
path-cost=10 pvid=100
add bridge=bridge comment=defconf interface=ether4 internal-path-cost=10
path-cost=10 pvid=100
add bridge=bridge comment=defconf interface=ether5 internal-path-cost=10
path-cost=10 pvid=100
add bridge=bridge comment=defconf interface=ether6 internal-path-cost=10
path-cost=10 pvid=100
add bridge=bridge comment=defconf interface=ether7 internal-path-cost=10
path-cost=10 pvid=100
add bridge=bridge comment=defconf interface=sfp-sfpplus1 internal-path-cost=
10 path-cost=10 pvid=100
/ip firewall connection tracking
set udp-timeout=10s
/ip neighbor discovery-settings
set discover-interface-list=MGMT
/interface bridge vlan
add bridge=bridge comment=VLAN100-HOME tagged=ether2,bridge untagged=
ether3,ether4 vlan-ids=100
add bridge=bridge comment=VLAN200-GUEST tagged=ether2 vlan-ids=200
add bridge=bridge comment=VLAN300-IOT tagged=ether2 vlan-ids=300
/interface list member
add comment=defconf interface=bridge list=LAN
add comment=defconf interface=ether1 list=WAN
add interface=pppoe-1 list=WAN
add interface=VLAN100-HOME list=MGMT
add interface=VLAN100-HOME list=ALLVLANS
add interface=VLAN200-GUEST list=ALLVLANS
add interface=VLAN300-IOT list=ALLVLANS
/interface ovpn-server server
add mac-address=FE:E6:6D:BD:99:C2 name=ovpn-server1
/interface wifi cap
set discovery-interfaces=LAN
/interface wifi capsman
set ca-certificate=auto enabled=yes interfaces=LAN package-path=“”
require-peer-certificate=no upgrade-policy=none
/interface wifi provisioning
add action=create-enabled disabled=no master-configuration=Home5Config
name-format=cap-wifi5GHz radio-mac=D4:01:C3:F8:18:66
slave-configurations=Home5Config-guest slave-name-format=
cap-wifi5GHz-guest supported-bands=5ghz-ax
add action=create-enabled disabled=no master-configuration=Home2.4Config-iot
name-format=cap-wifi2.4GHz radio-mac=D4:01:C3:F8:18:67 supported-bands=
2ghz-n
/ip address
add address=192.168.88.1/24 comment=defconf disabled=yes interface=bridge
network=192.168.88.0
add address=10.0.0.1/24 disabled=yes interface=bridge network=10.0.0.0
add address=10.0.0.1/24 interface=VLAN100-HOME network=10.0.0.0
add address=10.0.2.1/24 interface=VLAN200-GUEST network=10.0.2.0
add address=10.0.3.1/24 interface=VLAN300-IOT network=10.0.3.0
/ip dhcp-client
add comment=defconf disabled=yes interface=ether1
/ip dhcp-server lease
add address=10.0.0.2 client-id=1:d4:1:c3:f8:18:64 comment=“CAP AX”
mac-address=D4:01:C3:F8:18:64 server=home-dhcp
add address=10.0.0.10 client-id=1:30:9c:23:8b:8a:13 comment=“Main PC”
mac-address=30:9C:23:8B:8A:13 server=home-dhcp
add address=10.0.0.3 comment=ExampleHTTPSserver mac-address=74:56:3C:2B:43:4D server=
home-dhcp
/ip dhcp-server network
add address=10.0.0.0/24 dns-server=10.0.0.1 gateway=10.0.0.1
add address=10.0.2.0/24 comment=Guest dns-server=10.0.2.1 gateway=10.0.2.1
add address=10.0.3.0/24 comment=IOT dns-server=10.0.3.1 gateway=10.0.3.1
/ip dns
set allow-remote-requests=yes cache-size=30480KiB
/ip dns adlist
add ssl-verify=no url=“> https://raw.githubusercontent.com/hagezi/dns-blocklists>
/main/hosts/pro.txt”
/ip dns static
add address=10.0.0.1 comment=defconf name=router.lan type=A
add address=10.0.0.3 disabled=yes match-subdomain=yes name=snip.com
type=A
/ip firewall address-list
add address=10.0.0.0/24 list=allsubnets
add address=10.0.2.0/24 list=allsubnets
add address=10.0.3.0/24 list=allsubnets
/ip firewall filter
add action=drop chain=forward comment=“Drop InterVLAN” in-interface-list=
ALLVLANS out-interface-list=ALLVLANS
add action=accept chain=input comment=
“defconf: accept established,related,untracked” connection-state=
established,related,untracked
add action=drop chain=input comment=“defconf: drop invalid” connection-state=
invalid
add action=accept chain=input comment=“defconf: accept ICMP” protocol=icmp
add action=accept chain=input comment=
“defconf: accept to local loopback (for CAPsMAN)” dst-address=127.0.0.1
add action=drop chain=input comment=“Drop services access if not MGMT”
dst-port=80,8291,23,22,21,8729,8728 in-interface-list=!MGMT protocol=tcp
add action=accept chain=input comment=“Allow VLANS DNS tcp” dst-port=53
in-interface-list=ALLVLANS protocol=tcp
add action=accept chain=input comment=“Allow VLANS DNS udp” dst-port=53
in-interface-list=ALLVLANS protocol=udp
add action=drop chain=input comment=“defconf: drop all not coming from LAN”
in-interface-list=!LAN
add action=accept chain=forward comment=“defconf: accept in ipsec policy”
ipsec-policy=in,ipsec
add action=accept chain=forward comment=“defconf: accept out ipsec policy”
ipsec-policy=out,ipsec
add action=fasttrack-connection chain=forward comment=“defconf: fasttrack”
connection-state=established,related hw-offload=yes
add action=accept chain=forward comment=
“defconf: accept established,related, untracked” connection-state=
established,related,untracked
add action=drop chain=forward comment=“defconf: drop invalid”
connection-state=invalid
add action=drop chain=forward comment=
“defconf: drop all from WAN not DSTNATed” connection-nat-state=!dstnat
connection-state=new in-interface-list=WAN
/ip firewall nat
add action=dst-nat chain=dstnat comment=“Hairpin Forward” dst-address-list=
!allsubnets dst-address-type=local dst-port=443 in-interface=VLAN100-HOME
log-prefix=–dstnatHairpin-- protocol=tcp to-addresses=10.0.0.3 to-ports=30022
add action=masquerade chain=srcnat comment=“Hairpin NAT” dst-address=10.0.0.3
log-prefix=–srcnatHairpin-- protocol=tcp
add action=dst-nat chain=dstnat comment=“External forward” dst-port=443
in-interface-list=WAN protocol=tcp to-addresses=10.0.0.3 to-ports=30022
add action=masquerade chain=srcnat comment=“defconf: masquerade”
ipsec-policy=out,none out-interface-list=WAN
/ip ipsec profile
set [ find default=yes ] dpd-interval=2m dpd-maximum-failures=5
/ip smb shares
set [ find default=yes ] directory=flash/pub
/ipv6 firewall address-list
add address=::/128 comment=“defconf: unspecified address” list=bad_ipv6
add address=::1/128 comment=“defconf: lo” list=bad_ipv6
add address=fec0::/10 comment=“defconf: site-local” list=bad_ipv6
add address=::ffff:0.0.0.0/96 comment=“defconf: ipv4-mapped” list=bad_ipv6
add address=::/96 comment=“defconf: ipv4 compat” list=bad_ipv6
add address=100::/64 comment=“defconf: discard only " list=bad_ipv6
add address=2001:db8::/32 comment=“defconf: documentation” list=bad_ipv6
add address=2001:10::/28 comment=“defconf: ORCHID” list=bad_ipv6
add address=3ffe::/16 comment=“defconf: 6bone” list=bad_ipv6
/ipv6 firewall filter
add action=accept chain=input comment=
“defconf: accept established,related,untracked” connection-state=
established,related,untracked
add action=drop chain=input comment=“defconf: drop invalid” connection-state=
invalid
add action=accept chain=input comment=“defconf: accept ICMPv6” protocol=
icmpv6
add action=accept chain=input comment=“defconf: accept UDP traceroute” port=
33434-33534 protocol=udp
add action=accept chain=input comment=
“defconf: accept DHCPv6-Client prefix delegation.” dst-port=546 protocol=
udp src-address=fe80::/10
add action=accept chain=input comment=“defconf: accept IKE” dst-port=500,4500
protocol=udp
add action=accept chain=input comment=“defconf: accept ipsec AH” protocol=
ipsec-ah
add action=accept chain=input comment=“defconf: accept ipsec ESP” protocol=
ipsec-esp
add action=accept chain=input comment=
“defconf: accept all that matches ipsec policy” ipsec-policy=in,ipsec
add action=drop chain=input comment=
“defconf: drop everything else not coming from LAN” in-interface-list=
!LAN
add action=accept chain=forward comment=
“defconf: accept established,related,untracked” connection-state=
established,related,untracked
add action=drop chain=forward comment=“defconf: drop invalid”
connection-state=invalid
add action=drop chain=forward comment=
“defconf: drop packets with bad src ipv6” src-address-list=bad_ipv6
add action=drop chain=forward comment=
“defconf: drop packets with bad dst ipv6” dst-address-list=bad_ipv6
add action=drop chain=forward comment=“defconf: rfc4890 drop hop-limit=1”
hop-limit=equal:1 protocol=icmpv6
add action=accept chain=forward comment=“defconf: accept ICMPv6” protocol=
icmpv6
add action=accept chain=forward comment=“defconf: accept HIP” protocol=139
add action=accept chain=forward comment=“defconf: accept IKE” dst-port=
500,4500 protocol=udp
add action=accept chain=forward comment=“defconf: accept ipsec AH” protocol=
ipsec-ah
add action=accept chain=forward comment=“defconf: accept ipsec ESP” protocol=
ipsec-esp
add action=accept chain=forward comment=
“defconf: accept all that matches ipsec policy” ipsec-policy=in,ipsec
add action=drop chain=forward comment=
“defconf: drop everything else not coming from LAN” in-interface-list=
!LAN
/system clock
set time-zone-name=snip
/system note
set show-at-login=no
/system ntp client
set enabled=yes
/system ntp client servers
add address=snip
add address=0.europe.pool.ntp.org
add address=2.europe.pool.ntp.org
/system routerboard reset-button
set enabled=yes on-event=”/interface bridge set 0 vlan-filtering=no"
/tool mac-server
set allowed-interface-list=MGMT
/tool mac-server mac-winbox
set allowed-interface-list=MGMT

Disabling the drop intervlan rule seems to help, but I need that rule:

add action=drop chain=forward comment=“Drop InterVLAN” in-interface-list=
ALLVLANS out-interface-list=ALLVLANS

As I understand, this blocks interVLAN routing: VLAN100 to 200, 200 to 300 etc. But also to self: VLAN100 to VLAN100. This is normally not a problem since traffic does not get routed/firewalled in the same subnet. But in the case of Hairpin, I’m accessing a local server as if it’s remote (by domain), and so it hits the router. And it becomes: in-interface=VLAN100 and router sets out-interface=VLAN100, and there we go the rule above gets triggered.

The solution is to not use the catch all rule, but to explicitly block intervlan comms, per pair, both ways, e.g.: drop VLAN100 to VLAN200, drop VLAN200 to VLAN100, etc.
Does several rules (in comparison to previous one catch all rule) theoretically impact performance, or is it the same?

I wanted to leave this answer here in case someone runs into the same problem.

Bonus question if someone stumbles upon this, the solution (multiple rules), is there a neater solution, perhaps invert it? drop all forwarding, and then allow VLAN100 to VLAN100?

You can add

connection-nat-state=!dstnat connection-state=new

to that inter-vlan blocking rule, so that it becomes:

 
 /ip firewall filter
 add action=drop chain=forward comment="Drop InterVLAN" \
     connection-nat-state=!dstnat connection-state=new \
     in-interface-list=ALLVLANS out-interface-list=ALLVLANS

And it will exclude the connections that have been dstnat’ed (your port forwarding connections).

I didn’t know connection-nat-state filter exists, yes it works, probably the best solution.

It’s the condition used by the “defconf: drop all from WAN not DSTNATed” from MikroTik (that you can also see in your configuration). That’s how MikroTik allows you in the defconf configuration to simply add dstnat rules for port forwarding (from WAN) without having to individually add “accept” rules for all the forwarded ports in the filter table :blush:.

  1. Hairpin nat ONLY applies to users, attempting to use the server via its domain name, that are in the same SUBNET as the server.
    This does NOT apply to users on different vlans.

  2. Keep the hairpin nat rule simple
    add chain=srcnat action=masquerade src-address=serverSubnet dst-address=serverSubnet

Done!!

  1. The default rule alluded to, is great for an initial config but becomes completely useless or at least confusing to the new user.
    Much better, clearer and safer is the following
    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

    ************* here add any inter vlan access rules required *****************
    add chain=forward action=drop comment=“Drop all else”