Hairpin NAT With nftables
By chimo on (updated on )This is a short post about how I ended up implementing NAT hairpinning with nftables so that my Incus containers could talk to my other containers through their publicly accessible domains (in other words: “container to host to container”). All thanks to a helpful StackExchange post.
The StackExchange post describes the situation pretty well:
The reason why [a singular dnat] rule didn’t work is that while the destination address is changed to the correct one, and the answer will also go back to the expected source; the application running in app will expect an answer from the host, not [the container’s ip] - but that will be the case because of the DNAT.
The solution is to mark the connection, and when sending an answer back to app, we either need to add a SNAT, or MASQUERADE; so that the answer package gets the expected source address.
These are the commands I used to fix it in my case:
# Mark traffic from incus to the host on some ports
root@host:~# nft add rule ip nat prerouting iif $INCUS_IFC ip daddr $WAN_IP tcp dport { 80, 443, 587 } ct mark set 0x61687269 # DNAT to the target container
root@host:~# nft add rule ip nat prerouting iif $INCUS_IFC ip daddr $WAN_IP tcp dport { 80, 443, 587 } dnat $CONTAINER_IP # Pretend the marked traffic is coming from the host with SNAT
root@host:~# nft add rule ip nat postrouting oif $INCUS_IFC ct mark 0x61687269 snat to $WAN_IP