Skip to content

fix(client): keep split-tunnel DNS routes across a reconnect - #316

Open
Diffechento wants to merge 1 commit into
fptn-project:masterfrom
Diffechento:fix/split-tunnel-dns-routes-survive-reconnect
Open

Diffechento wants to merge 1 commit into
fptn-project:masterfrom
Diffechento:fix/split-tunnel-dns-routes-survive-reconnect

Conversation

@Diffechento

@Diffechento Diffechento commented Jul 26, 2026

Copy link
Copy Markdown

In domain-based split-tunnel mode some destinations that should bypass the VPN intermittently start going through the tunnel instead. In my setup .ru sites like ozon.ru and 2ip.ru would randomly begin exiting via the VPN and then recover on their own a while later.

The split-tunnel host routes discovered from DNS live in RouteManager::dns_routes_ipv4_/ipv6_ and are added on the fly by AddDnsRoutesIPv4/IPv6. The server drops the session every few hours (I see a websocket timeout roughly every ~3h), and on each disconnect Supervise() does a full restart: Clean() then Apply(). Clean() deletes and clears the dns_routes_* sets, but Apply() only re-adds the static exclude/include networks from config -- it never restores the domain routes. So after a reconnect a /32 comes back only once that domain is resolved again; domains still sitting in a client's DNS cache have no direct route and fall back to the VPN default until their TTL expires. The IP-based EXCLUDE_TUNNEL_NETWORKS don't hit this because Apply() re-adds them right away, which is why only the domain routes leaked.

The fix keeps the DNS routes across a reconnect instead of rebuilding them lazily:

Clean(keep_dns_routes=true)    // reconnect teardown: drop system routes, keep the entries
Apply() -> ReapplyDnsRoutes()  // re-install the kept entries once the tunnel is back

Only the reconnect path in Supervise() passes keep_dns_routes=true; full shutdown and the give-up path keep the original full clear. ReapplyDnsRoutes() runs under the lock Apply() already holds (like AddExcludeNetworks) and reuses the same interface/gateway selection as AddDnsRoutes*. During the reconnect window Clean() already restores the direct default route, so nothing leaks while the tunnel is down, and the split-tunnel routes are back the moment Apply() finishes -- no waiting on a fresh DNS query.

I confirmed the root cause on a live Linux client (PPPoE, exclude mode): the journal shows the ~3h websocket timeout and Full VPN restart, each followed by a bulk ip route del of the split-tunnel /32s, after which a cached .ru address resolves to the tun0 default until it's looked up again.

Heads up: I haven't built or run this branch yet -- it's by code inspection against that flow. It needs a Linux build plus a forced-reconnect check that the routes come back without a new DNS query, and a look at the macOS/Windows re-add paths (same AddIPv4/IPv6RouteToSystem helpers). Happy to iterate.

On every full VPN restart the supervisor called RouteManager::Clean(),
which deletes and clears the on-the-fly split-tunnel host routes stored
in dns_routes_ipv4_/ipv6_. Apply() only re-adds the static config-based
exclude/include networks, so the domain-resolved /32 routes were gone
until each domain happened to be looked up again.

The server closes the session periodically (observed ~every 3h), so
after each reconnect any split-tunnel destination whose address was
still cached on a client (no fresh DNS query) lost its direct route and
fell back to the VPN default route -- e.g. .ru sites started exiting
through the tunnel until their TTL expired.

Fix: on a reconnect teardown keep the DNS route entries
(Clean(keep_dns_routes=true)) and re-install them at the end of Apply()
via ReapplyDnsRoutes(), so split-tunnel routing is restored immediately
when the tunnel comes back instead of lazily per DNS query. Full
shutdown and the give-up path still call Clean() with a full clear.
@Diffechento
Diffechento force-pushed the fix/split-tunnel-dns-routes-survive-reconnect branch from 69c7b84 to 6aadb8f Compare July 26, 2026 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant