Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

Split tunnel w/o changing route table

I've built a VPN app that is based on wireguard on macOS (I have both AppStore ver. and Developer ID ver). I want to achieve split tunneling function without changing the system route table.

Currently, I'm making changes in PacketTunnelProvider: NEPacketTunnelProvider. It has included/excluded routes that function as a split tunnel, just that all changes are immediately reflected on the route table: if I run

netstat -rn

in terminal, I would see all rules/CIDRs I added, displayed all at once. Since I have a CIDR list of ~800 entries, I'd like to avoid changing the route table directly.

I've asked ChatGPT, Claude, DeepSeek, .etc. An idea was to implement an 'interceptor' to

intercept all packets in packetFlow(_:readPacketsWithCompletionHandler:), extract the destination IP from each packet, check if it matches your CIDR list, and either reinject it back to the system interface (for local routing) or process it through your tunnel.

Well, LLMs could have hallucinations and I've pretty new to macOS programming. I'm asking to make sure I'm on the right track, not going delusional with those LLMs :) So the question is, does the above method sounds feasible? If not, is it possible to achieve split tunneling without changing the route table?

Answered by DTS Engineer in 841856022

Your request is strange: You want to change how the system routes packets without modifying the routing table, but the routing table is how the system determines how to route packets. How are you expecting that to work? [1]

Regarding this:

reinject it back to the system interface (for local routing)

NEPacketTunnelFlow doesn’t support re-injecting packets. If a packet is routed to your provider, you are responsible for getting it to its destination.

Share and Enjoy

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

[1] There’s an old joke that macOS must have two of everything (-: In many respects that’s true, but not here. There’s only one mechanism to route packets based on their source and destination addresses.

Your request is strange: You want to change how the system routes packets without modifying the routing table, but the routing table is how the system determines how to route packets. How are you expecting that to work? [1]

Regarding this:

reinject it back to the system interface (for local routing)

NEPacketTunnelFlow doesn’t support re-injecting packets. If a packet is routed to your provider, you are responsible for getting it to its destination.

Share and Enjoy

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

[1] There’s an old joke that macOS must have two of everything (-: In many respects that’s true, but not here. There’s only one mechanism to route packets based on their source and destination addresses.

Hi Quinn,

Thank you for the reply that clarifies my 'hallucination'.

I know the whole thing sounds a little bit strange. However, on linux (ubuntu), we have

ip rule

which is independent of route table; on windows, we have 'Windows Filter Platform' -- to be honest I haven't looked into it yet, but it supposedly can filter packets and redirect them into different TUN interface without changing the route table.

If macOS have two of everything (I know it doesn't know xD), how could it not have a way to determine package routing manually?

Do we have any alternative at all? For example, configure anchor in pf.conf. I'm also a little bit curious about other VPN's split tunneling function. Do they not exist/not work on macOS at all?

Accepted Answer
supposedly can filter packets and redirect them into different TUN interface without changing the route table.

Apple platform do have something like this — like I said, two of everything! (-: — namely NECP. For some very limited details on that, see A Peek Behind the NECP Curtain. However, as a third-party developer you have a very limited view into NECP, and it certainly won’t help with this.

The go-to doc on this is Routing your VPN network traffic. It explains:

  • How includeAllNetworks represents a giant switch, which effectively disables destination IP routing. This setting is implemented using NECP.

  • How enforceRoutes lets you continue using destination IP routing but prevents apps from bypassing that.

I'm also a little bit curious about other VPN's split tunneling function. Do they not exist/not work on macOS at all?

They use the routing table [1].

That’s part of what I don’t understand here: Why not just achieve your goal using the routing table? That’s what it’s there for.

Did you try to do this and have it fail on you?

Share and Enjoy

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

[1] Well, they do on iOS. Many macOS VPN products predate Network Extension framework, and thus were implemented using a variety of ad-hoc techniques. These days we have NE and thus DTS no longer supports such shenanigans.

One specific case of that is PF, which is the subject of TN3165 Packet Filter is not API.

Thanks for all the info! I could use routing table and it works perfectly. I was required to find a way not to using it somehow, but now with your reply, I can definitely try to flip the table with confidence. Quinn you're the best :)

Split tunnel w/o changing route table
 
 
Q