How to do IP masquerading on MacOS 10.14+?

Solution 1:

— Mac OS' Pf won't do this for you. Here's why:

If you'd take a look at its manual you'd find that NAT is happening before filtering. But NAT rules don't support all variety of hall-marks as filtering rules do. Namely there's no way to check for socket's ownership while doing NAT. You can restrict NAT rules' applicability with, say, source or destination IPs, but not ownership.

Another thing to mention is during NAT processing Pf's doing normal route look up. It means you won't have nat on en0 working at all — the packets are routed according to kernel's routing table at that moment. In your case they are dispatched to be sent via default route's interface which is VPN interface. And they would be using VPN interface's address as theirs source IPs — which is not surprising for normal route look ups, but obviously isn't playing along with your plan.

To summarise the contradictions, briefly:

  • If you don't do NAT you have wrong Source IP when route-to is applied
  • Your NAT rule should be set on default route interface (VPN) while changing Source IP to IP of non-VPN interface, like: nat on vpn0 … -> (en0)
  • But OTOH you can't have custom NAT (by ownership) and if you do NAT anyways then traffic that is supposed to go via VPN would have wrong Source IP.

P. S. The actual state of things in Mac OS's Pf is even worse. After NAT is done ownership matching won't work in filtering rules either.