How to restore macOS routing table after VPN crash or routing changes?

Hi, I have a VPN product for macOS. When activated, it creates a virtual interface that capture all outgoing traffic for the VPN. the VPN encrypt it, and send it to the tunnel gateway. The gateway then decapsulates the packet and forwards it to the original destination.

To achieve this, The vpn modifies the routing table with the following commands:

# after packets were encoded with the vpn protocol, re-send them through 
# the physical interface
/sbin/route add -host <tunnel_gateway_address_in_physical_subnet> <default_gateway> -ifp en0 > /dev/null 2>&1

# remove the default rule for en0 and replace it with scoped rule 
/sbin/route delete default <default_gateway> -ifp en0 > /dev/null 2>&1
/sbin/route add default <default_gateway> -ifscope en0 > /dev/null 2>&1

# create new rule for the virtual interface that will catch all packets 
# for the vpn 
/sbin/route add default <tunnel_gateway_address_in_tunnel_subnet> -ifp utunX > /dev/null 2>&1

This works in most cases. However, there are scenarios where the VPN process may crash, stop responding, or another VPN product may alter the routing table. When that happens, packets may no longer go out through the correct interface.

Question: Is there a way to reliably reconstruct the routing table from scratch in such scenarios? Ideally, I would like to rebuild the baseline rules for the physical interface (e.g., en0) and then reapply the VPN-specific rules on top. Are there APIs, system utilities, or best practices in macOS for restoring the original routing configuration before reapplying custom VPN routes?

Thanks

Answered by DTS Engineer in 859211022
I have a VPN product for macOS.

It sounds like your VPN product isn’t based on the Network Extension (NE) provider architecture. Is that right?

If so, there’s not much I can do to help you out here. On macOS the routing table is ‘owned’ by system infrastructure. You can’t safely modify it behind the system’s back. If you do, you’ll run into all sorts of weird problems.

This has been true since the dawn of Mac OS X but, historically, you had to bend the rules to create a VPN product. That’s not been the case since we introduced support for NE providers back in macOS 10.11. At that point DTS stopped supported such shenanigans; we now only support VPN products based on the NE provider architecture.

This year at WWDC the NE team went out of their way to clarify their position on this. See WWDC 2025 Session 234 Filter and tunnel network traffic with NetworkExtension, starting at 7:26.


Oh, and if you are using an NE provider and NE is failing to restore the routine table properly, please do let me know. That should just happen automatically.

Share and Enjoy

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

I have a VPN product for macOS.

It sounds like your VPN product isn’t based on the Network Extension (NE) provider architecture. Is that right?

If so, there’s not much I can do to help you out here. On macOS the routing table is ‘owned’ by system infrastructure. You can’t safely modify it behind the system’s back. If you do, you’ll run into all sorts of weird problems.

This has been true since the dawn of Mac OS X but, historically, you had to bend the rules to create a VPN product. That’s not been the case since we introduced support for NE providers back in macOS 10.11. At that point DTS stopped supported such shenanigans; we now only support VPN products based on the NE provider architecture.

This year at WWDC the NE team went out of their way to clarify their position on this. See WWDC 2025 Session 234 Filter and tunnel network traffic with NetworkExtension, starting at 7:26.


Oh, and if you are using an NE provider and NE is failing to restore the routine table properly, please do let me know. That should just happen automatically.

Share and Enjoy

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

Thanks for your response Quinn @DTS Engineer . You’re correct, my VPN isn’t based on a Network Extension provider, but I do use a Transparent Proxy to divert some traffic outside the tunnel.

Based on your suggestion, I’m considering building entirely on the NetworkExtension framework with two providers:

  1. NETransparentProxyProvider – intercepts app sockets so I can decide what to bypass.
  2. NEPacketTunnelProvider – encapsulates and encrypts the rest according to my VPN protocol.

My goal is that each TCP/UDP socket first reaches the Transparent Proxy callback, and if diverted to the tunnel, then it flows the data through the Packet Tunnel provider for encapsulation before hitting the physical adapter.

Is this the expected pipeline when both providers are active ? 

Thanks !

Is this the expected pipeline when both providers are active ?

Traffic will certainly hit the transparent proxy first. It has two choices:

  • If it returns false from its handle-new-flow method, the system acts as if the transparent proxy wasn’t installed.
  • It it returns true, the proxy then becomes responsible for handling the flow.

It’s common for the transparent proxy to directly proxy the connection, that is, make its own connection and then forward traffic from the between the two. The challenge here is that, if the proxy connects directly, that traffic will be seen by the packet tunnel and you won’t have bought yourself anything.

There are various things you can do here, but the best option very much depends on the overall structure of your product. Specifically:

  • Do you actually have a VPN server?
  • If so, what criteria are you using to determine whether to send traffic to the server?

Share and Enjoy

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

Hi Quinn @DTS Engineer, thanks for clarifying.

I do have a dedicated VPN server. My TransparentProxyProvider base its routing on flow details (for example NEAppProxyTCPFlow), deciding whether to send traffic directly or encapsulate it via the Packet Tunnel provider. I handle this by creating the socket to the remote endpoint and binding it either to the tunnel IP or the physical interface IP. Now, if I want to move to Network Extension (NE) provider instead of directly generate the vitual interface (utun), how can I acquire this information (the virtual adapter ip address).

if I want to move to [NE], how can I acquire this information (the virtual adapter ip address)

When a packet tunnel provider starts, it calls setTunnelNetworkSettings(_:completionHandler:) to configure the interface. That configuration includes the IP address, so you’re asking how to get information that you already know (-:

However, I generally recommend that you bind to the interface rather than binding to its IP address. You can get the interface from the virtualInterface property on the packet tunnel provider object.

There are two things to note about this:

  • The type is different depending on whether you’re using Objective-C or Swift. See NWEndpoint History and Advice for the backstory there.
  • This is only available on macOS 15 and later. On older systems you should find the interface by calling getifaddrs and looking for the interface which has your IP address.

Share and Enjoy

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

How to restore macOS routing table after VPN crash or routing changes?
 
 
Q