Add a default route through the tunnel and the tunnel's own packets follow it. The table then contains a rule that defeats itself. The encrypted connection to the server tries to travel inside the connection it is trying to establish.

Two mechanisms prevent that loop. One is visible in the routing table, the other lives inside the application.

Inner Traffic and Outer Traffic Are Different Decisions

An application packet enters the virtual interface and gets encapsulated. The result is a new packet addressed to the server's public address. That packet needs a route on the physical network, because nothing about it may enter the tunnel.

Both are routing decisions, and only the first one belongs to the tunnel. When the client installs a default route that also covers the server, the outer packet is captured as well.

The symptoms are immediate and total. The handshake completes and then nothing flows, or the connection drops the instant the route is installed. Either way the keepalive can no longer leave.

Option One: A Host Route for the Endpoint

The direct approach adds a route for the server's address through the original gateway. It takes one line. It is visible to anyone reading the table. It is also easy to explain in a change record.

Its weakness appears when the underlying path changes. The saved route names a gateway and an interface from the old network. After a Wi-Fi switch that gateway may not exist, and the route stays behind, looking correct and doing nothing.

# before the tunnel: which path reaches the endpoint?
ip route get 198.51.100.7

# pin it through the current gateway
ip route add 198.51.100.7/32 via 192.168.1.1 dev wlan0

# after the tunnel is up, confirm the pin survived
ip route get 198.51.100.7

A hostname makes this harder. If the endpoint is a name that resolves to several addresses, the exclusion has to cover the address actually in use. A later lookup can return something the route never covered.

Option Two: Protect the Socket

The other approach never touches the routing table. The client marks or protects the socket that carries the tunnel, so the operating system sends that socket's traffic outside the tunnel by policy. Android exposes the mechanism directly to VPN applications. Desktop clients implement equivalents with firewall marks or bind-to-device calls.

The advantage is that the exception follows the connection rather than a saved gateway. A network change rewrites the underlying path, and the protected socket moves with it because it was never pinned to an address. The route table stays clean.

The cost is visibility. Nothing in the routing table shows the exception, so a reviewer looking for a host route finds nothing and may conclude the design is unsafe. The protection lives in the client's code and in the policy rules that code installs.

Choosing Between Them

Pick the socket-level mechanism when the client supports it, because it survives the network changes that break a pinned route. Pick the host route when the exception has to be visible to other tools, or when the client offers nothing better.

Neither mechanism removes the need for the server address to be reachable at all. A private gateway has a public listener address, and that address is what must stay outside the tunnel. The internal networks behind it become reachable only after the tunnel exists.

Whichever you choose, keep it with the component that owns the tunnel. A stray host route added by hand works until the next network, and then it becomes tomorrow's mystery outage.

Pennadoù liammet