The pf_route() functions bypass the normal routing mechanism thus need to re-implement some functionalities on their own, like fragmentation, ICMP signalling and testing the outbound firewall. Because of this for route-to targets functions depending on pf_send_tcp(), like syncookies, won't work and the dtrace probe for pf_test() is never called and packets delayed by dummynet need to be routed and emitted again. The current approach also skips things like ipsec.
This draft proposes the following solution: have the pf_route() and pf_route6() functions use the PACKET_TAG_IPFORWARD tag and let the FreeBSD IP stack handle the routing. This unfortunately does not work out of the box, see D41479. Furthermore the change breaks the ability to specify the interface for the redirection pools - redirection target must be a reachable address and a matching interface will be used. This patch does not deal with removing the interface specification from pf.conf and other structures like pfsync. OpenBSD has already removed the ability to force the interface and greatly simplified the redirection pool logic some years ago. I've already looked into that as a part of my NAT64 backport effort and when I have time to get back to it I'd like to port the better redirection pools and remove the interface from them anyway.
At the moment of writing this the patch breaks one test: pf/route_to:icmp_nat. After some investigation I must say that handling of post-NAT ICMP is broken in pf in general. A ruleset with no route-to, just plain forwarding and a NAT rule on an outbound interface of the router jail with small MTU will cause exactly the same issue. The function icmp_error eventually results in a call to ip_output which will only call pfil_mbuf_out which would only match the inbound pf state of the router jail. Maybe we should call pfil_mbuf_in from icmp_send?
Also I don't understand why the test uses pass out route-to instead of pass in route-to, as with such approach the gateway and possible the interface will be changed after a state is already created on another interface. With set state-policy if-bound such ruleset could not work anyway if the interface is changed.
Sponsored by InnoGames GmbH