Skip to content

Fail over to a different Mullvad/IVPN relay when the current one stays dead - #688

Merged
kasnder merged 2 commits into
masterfrom
claude/github-issue-679-2zbcql
Jul 28, 2026
Merged

Fail over to a different Mullvad/IVPN relay when the current one stays dead#688
kasnder merged 2 commits into
masterfrom
claude/github-issue-679-2zbcql

Conversation

@kasnder

@kasnder kasnder commented Jul 28, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #679 — "Mullvad stops when server becomes unavailable".

  • WgEgress already 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.
  • Mullvad/IVPN each expose many relay servers, and TC already knows how to pick one (MullvadProfileGenerator/IvpnProfileGenerator), but that machinery was only wired into the manual "create a profile" UI flow.
  • Added WgRelayFailover, a small provider-aware helper: after several consecutive full-restart cycles have all failed to recover, WgEgress now 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.
  • Self-hosted or manually imported single-server configs are left untouched — there's nothing to fail over to for those, so WgRelayFailover no-ops for any profile with an empty provider.
  • If the failover attempt itself fails (e.g. no network), WgEgress falls back to the pre-existing same-server backoff, so behavior is never worse than before.

Test plan

  • No Android SDK is available in this sandbox, so the change could not be compiled/tested locally; please run CI and, ideally, exercise it manually against a Mullvad/IVPN account by blackholing the current relay's IP.
  • ./gradlew :app:compileGithubDebugJavaWithJavac
  • ./gradlew :app:testGithubDebugUnitTest
  • Manually verify: with a Mullvad WireGuard profile active, block the current relay's IP (e.g. via router/firewall) and confirm TC switches to a different relay after a few restart cycles instead of retrying the dead one indefinitely.

Generated by Claude Code

claude added 2 commits July 28, 2026 07:24
…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.
@kasnder
kasnder marked this pull request as ready for review July 28, 2026 08:24
@kasnder
kasnder merged commit 2f3cee4 into master Jul 28, 2026
4 checks passed
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.

Mullvad stops when server becomes unavailable

2 participants