There do appear to be contradictions in documentation as well as the pf book. The Configuring NAT section is correct as you have seen with your own rules.
Here is the minimum set of stateless rules that allows ICMP traffic between my laptop and Cloudflare. # Options. set block-policy drop # Macros. wan = em0 wifi = vlan4 external = 76.154.165.21 laptop = 192.168.6.2 cflare = 1.1.1.1 # NAT. match out on $wan inet proto icmp from $laptop nat-to $external static-port # Filtering rules. pass in quick on $wifi inet proto icmp from $laptop to $cflare no state pass out quick on $wan inet proto icmp from $external to $cflare keep state (if-bound) pass out quick on $wifi inet proto icmp from $cflare to $laptop no state block quick The second filter rule _must_ be stateful in order for the router to map the ICMP Query ID back to the original source IP (i.e., my laptop). When relying on only stateful rules, we can remove the last pass rule- since the first filter rule when hit will allow the returning Echo Response. If instead of using match rules for NAT, you use pass out; then pf will fail to load pf.conf(5) if the second rule is stateless: /etc/pf.conf:13: nat-to and rdr-to require keep state /etc/pf.conf:13: skipping rule due to errors /etc/pf.conf:13: rule expands to no valid combination pfctl: Syntax error in config file: pf rules not loaded If you want to easily distinguish traffic sourced from your router leaving em0 from traffic sourced from your (V)LAN devices, then you will need to have a separate rule. For example: pass out quick log on em0 inet from (em0) match out log on em0 inet from athn0:network nat-to (em0) pass out log quick on em0 inet