Fail over to a different Mullvad/IVPN relay when the current one stays dead - #688
Merged
Conversation
…s dead WgEgress already retries a broken tunnel with backoff, but every retry re-resolved and restarted against the same fixed [Peer] endpoint baked into the profile. If the relay server itself is down (not just a local network blip), that loop never recovers. After a few consecutive full-restart cycles keep failing to produce any traffic, WgEgress now asks a new provider-aware hook (WgRelayFailover) to move the active Mullvad/IVPN profile to a different relay, reusing the existing key material, before continuing the restart loop. Self-hosted or manually imported profiles are left untouched, since there is only one server to fail over to. Fixes #679
- Bound the relay-list fetch with a real timeout (FAILOVER_TIMEOUT_MS): previously forceRestartPending blocked every other recovery path while the unbounded network call ran, so a hang could stall recovery longer than not having failover at all. - Make failoverInFlight an AtomicBoolean to close a check-then-set race between the monitor and rebind threads. - Only trigger failover from monitor-observed breaks, not from rebind failures after a network change (those aren't evidence the relay is dead). - Reset the restart backoff after a successful switch so a fresh relay doesn't inherit the old one's penalty. - Persist the IVPN session WgRelayFailover generates/reuses; it was minting sessions that never got saved, orphaning them against the account's device limit and desyncing stored session state from the live config. - Scope the config write to the profile ID resolved before the network call (WgProfileManager#updateProfileConfigIfActive), so a profile switch mid-fetch can't overwrite a different profile's stored config. - Exclude the relay that just failed from the next pick when another candidate exists, and reject a pick that silently widens the exit country, for both Mullvad and IVPN. - Distinguish ApiRejectedException/CaptchaRequiredException from other failures in the failover log. - Add WgRelayFailoverTest and extend WgProfileManagerTest for the new guard clauses.
3 tasks
kasnder
marked this pull request as ready for review
July 28, 2026 08:24
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.
Summary
Fixes #679 — "Mullvad stops when server becomes unavailable".
WgEgressalready retries a broken WireGuard tunnel with backoff, but every retry (cheap rebind and full restart alike) re-resolved and restarted against the exact same[Peer]endpoint baked into the profile at creation time. If the relay server itself goes down (rather than a local network blip or IP change), that loop can never recover — it just keeps hammering the same dead server, backing off up to 5 minutes between tries, forever.MullvadProfileGenerator/IvpnProfileGenerator), but that machinery was only wired into the manual "create a profile" UI flow.WgRelayFailover, a small provider-aware helper: after several consecutive full-restart cycles have all failed to recover,WgEgressnow asks it to move the active profile to a different relay (same provider/account/country, reusing existing key material) before continuing the restart loop. This requires one network call (a relay-list fetch), so it runs on a dedicated background thread.WgRelayFailoverno-ops for any profile with an emptyprovider.WgEgressfalls back to the pre-existing same-server backoff, so behavior is never worse than before.Test plan
./gradlew :app:compileGithubDebugJavaWithJavac./gradlew :app:testGithubDebugUnitTestGenerated by Claude Code