fix(client): keep split-tunnel DNS routes across a reconnect - #316
Open
Diffechento wants to merge 1 commit into
Open
Diffechento wants to merge 1 commit into
Diffechento wants to merge 1 commit into
Conversation
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
force-pushed
the
fix/split-tunnel-dns-routes-survive-reconnect
branch
from
July 26, 2026 10:30
69c7b84 to
6aadb8f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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 delof 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.