One line in a requirements document, "this app may use the VPN, and nothing else", quietly asks for two mechanisms that no single switch provides. Traffic needs a path through the intended tunnel, and traffic that would leave directly needs to be refused. Tools that do one of those well and the other not at all are common, which is why a comparison that only asks whether something can block answers half the question confidently. Write the boundary down first, then choose the components that cover both halves of it.

Two requirements hiding inside one sentence

Steering is the part that puts a connection into the tunnel or the proxy. Refusal is the part that stops the same connection from leaving by any other route. Different components implement them, they fail in different ways, and a product can be excellent at one while saying nothing about the other:

ApproachSteers trafficRefuses direct egressKnows the app identity
Per-app VPN profileYes, for the apps it namesOnly if a filter or firewall also blocksYes, within its own rules
Application proxy settingPartly, for requests that honour itNoNo
Content filterNoYesYes, if the filter is written that way
Local PF rulesetNoYes, by address and portNot reliably

Read the table as a checklist rather than a ranking. No single row fills both middle columns, and the rows that do cover them are two components cooperating: one owns the path, the other owns the refusal. The identity column matters for a different reason, because a rule that cannot name the app cannot enforce a rule about that app. Whatever you deploy has to answer all three, or the boundary in the requirements document is not the boundary on the machine.

Steering on its own leaves a way out

An application proxy setting is the clearest example of steering without refusal. It can steer requests from an app that honours the system proxy, and it covers nothing else: a helper process, an updater, a background service or a different protocol can travel on its own path without ever reading that preference. Choosing a SOCKS endpoint in one pane therefore does not create a no-direct-egress policy for the application. If the requirement is about the app as a whole, ask which components the rule actually identifies.

A per-app VPN profile steers better than a proxy, because the platform decides which apps enter the tunnel instead of each app deciding for itself. It still refuses nothing: the profile supplies a path, and traffic that fails to match it takes the ordinary route. That gap is exactly where a second component earns its place, and it is why products that look like one switch are usually two.

Refusal on its own breaks the app

Apple's content-filter documentation describes an allow-or-block verdict on traffic as it passes through the network stack. That verdict is not a tunnel and not a proxy connection, because it decides whether something may continue rather than where it goes. A filter on its own enforces the refusal perfectly and leaves the application unusable, with clean logs and no explanation. A filter verdict can stop a connection but never supplies the tunnel, so pairing it with steering is the design decision rather than an optional extra.

A locally administered PF ruleset sits at the far end of that spectrum. It is a legitimate support decision on a machine an administrator owns, and it is not automatically the foundation of a distributable product, since whoever owns the ruleset also owns its interactions with other firewall users, its upgrade path and its recovery when a rule is wrong. Adding PF to a product does not remove that ownership question; it defines it.

Deployment decides whether the rule still exists tomorrow

How a Network Extension is deployed on Apple platforms depends on the provider type, the packaging and the OS version in front of you. An app extension on macOS runs in a user context and ends when that user logs out, while a system extension has a global context that does not depend on the session. If protection must survive a logout, that difference is the whole design rather than a footnote. Confirm the deployment route before promising the behaviour, because the same code lands differently depending on which extension type ships.

Source availability is a separate matter from enforcement. Reading a repository tells you how something is implemented, not what the installed binary is allowed to do on this machine. Signing, installation and authorization still apply, and instructions built around an obsolete kernel extension or a library-injection trick describe a different platform and a different risk posture, however well they once worked.

An acceptance case that names the app

Pick a single action the application performs on its own, note the address it should reach and whether that address is IPv4 or IPv6, then repeat the same action in the approved no-tunnel state. The second run has to produce the declared refusal rather than a quiet direct retry. That is the half people leave unspecified, and the only half that proves refusal exists at all. Run it with the tunnel available first, so a difference between the two results can only come from the missing tunnel.

Then walk the transitions the requirement names: restarting the app, a helper firing, the tunnel reconnecting, the machine changing networks, and a logout or reboot where those apply. A browser public-IP page is not a test of another application's sockets, so it cannot stand in for this one. Where no supported deployment covers the app's real behaviour, an authorized isolated environment with an explicit gateway and no alternative egress is a containment decision with its own management cost, not a checkbox replacement.

Finish by writing down four entries: which components make up the app, who owns steering, who owns refusal, and the states in which the rule must still hold. A public repository can be one selection criterion, but it is not one of the four, and those entries are what settle whether a given tool meets the networking requirement.

Akụkọ ndị metụtara ya