Replacing Packet Filter (pf) with Content Filter for VPN Firewall Use Case

Hi,

We're in the process of following Apple’s guidance on transitioning away from Packet Filter (pf) and migrating to a Network Extension-based solution that functions as a firewall. During this transition, we've encountered several limitations with the current Content Filter API and wanted to share our findings.

Our VPN client relies on firewall functionality to enforce strict adherence to split tunneling rules defined via the routing table. This ensures that no traffic leaks outside the VPN tunnel, which is critical for our users for a variety of reasons.

To enforce this, our product currently uses interface-scoped rules to block all non-VPN traffic outside the tunnel. Replicating this behavior with the Content Filter API (NEFilterDataProvider) appears to be infeasible today.

The key limitation we've encountered is that the current Content Filter API does not expose information about the network interface associated with a flow. As a workaround, we considered using the flow’s local endpoint IP to infer the interface, but this data is not available until after returning a verdict to peek into the flow’s data—at which point the connection has already been established. This can result in connection metadata leaking outside the tunnel, which may contain sensitive information depending on the connection.

What is the recommended approach for this use case?

  • NEFilterPacketProvider?
    This may work, but it has a negative impact on network performance.
  • Using a Packet Tunnel Provider and purely relying on enforceRoutes?
    Would this indeed ensure that no traffic can leak by targeting a specific interface or by using a second VPN extension?

And more broadly—especially if no such approach is currently feasible with the existing APIs—we're interpreting TN3165 as a signal that pf should be considered deprecated and may not be available in the next major macOS release. Is that a reasonable interpretation?

Answered by DTS Engineer in 839708022

My go-to reference for this sort of stuff is Routing your VPN network traffic [1]. This outlines two options for preventing ‘escapes’:

  • For a full tunnel, set includeAllNetworks and then set exceptions via the various excludeXYZ options.

  • For a split tunnel, set enforceRoutes. There’s some subtlety with this, as explained in the Enforce the inclusions and exclusions for a packet tunnel provider section.

That leaves this:

be able to block any traffic while the tunnel is not yet connected

Honestly, I’m not sure what the best path forward is for that requirement. I’m gonna research that and get back to you.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] I know you’ve seen this but I’m replying as if you haven’t so that our conversation makes sense to other folks reading this thread.

we're interpreting TN3165 as a signal that pf should be considered deprecated

Well, we can’t deprecate something that we’ve never officially supported as an API (-: But, yes, our position is that, if you’re building Mac networking products using legacy techniques then you should move to a Network Extension provider. That includes PF, but other things as well, including manually manipulating UTUN interfaces and modifying the routing table ‘behind the back’ of configd.

may not be available in the next major macOS release

We’ve made no announcement about timelines. However, our experience is that products using these legacy techniques tend to experience compatibility problems as we evolve the system, so it’d be better to move off them sooner rather than later.


Coming back to your content filter question, I don’t have an immediate answer for you. I’m gonna do some digging and get back to you.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I’m gonna do some digging and get back to you.

Actually, scratch that. Before I start digging deeper, I’d like to get a better handle on the goals of your product.

When I first read your post I was focused on the content filter side of this, but there’s also a VPN side. You wrote:

This ensures that no traffic leaks outside the VPN tunnel

So it sounds like:

  • You’re actually building a VPN product.

  • Specifically, it operates at the IP packet layer.

  • Forwarding IP packets via a tunnel to a VPN server.

  • Historically it was hard to prevent ‘leaks’ from the tunnel.

  • So you did that using PF.

Is that right?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks for the quick responses so far!

Yes, the product basically provides an IP-Layer VPN to a remote server.

PF is indeed used to enforce all outgoing traffic complies with the configured split tunnel rules and traffic isn't leaked. These leaks would mainly be due to:

  • connections that ignore the routing table and enforce an interface (e.g. curl --interface en0 https://apple.com)
  • be able to block any traffic while the tunnel is not yet connected

My go-to reference for this sort of stuff is Routing your VPN network traffic [1]. This outlines two options for preventing ‘escapes’:

  • For a full tunnel, set includeAllNetworks and then set exceptions via the various excludeXYZ options.

  • For a split tunnel, set enforceRoutes. There’s some subtlety with this, as explained in the Enforce the inclusions and exclusions for a packet tunnel provider section.

That leaves this:

be able to block any traffic while the tunnel is not yet connected

Honestly, I’m not sure what the best path forward is for that requirement. I’m gonna research that and get back to you.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] I know you’ve seen this but I’m replying as if you haven’t so that our conversation makes sense to other folks reading this thread.

I’m gonna research that and get back to you.

My initial thought on this front was to create a packet tunnel provider that starts and stays started regardless of the state of the underlying tunnel. The system will then route traffic for the destination network to you, and you can just drop it if the tunnel is down.

The drawback to this approach is that there isn’t a good way to start the tunnel at boot. We actually discussed this on the forums recently — see this thread — and there are approaches that work but nothing that I’d consider to be ‘good’.

Oh, and the developer in that thread filed an ER, which is great. If you want to track its state, file your own bug and ask that it be dup’d to that one.

An alternative approach is to create an NE filter data provider and have it and your packet tunnel provider cooperate to implement this feature. That is, when a new flow arrives at the filter, check whether the tunnel is up and, if it isn’t, block the flow.

Note that, if you use sysex packaging, which you really should, both NE providers run in the same process and thus this sort of cooperation is easy. It can be something as simple as a global variable (with ån appropriate mutex, of course).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Replacing Packet Filter (pf) with Content Filter for VPN Firewall Use Case
 
 
Q