Skip to content

Fix WireGuard rotation edge cases (#766) - #774

Merged
kasnder merged 1 commit into
masterfrom
fix/wg-rotation-edge-cases
Aug 22, 2026
Merged

Fix WireGuard rotation edge cases (#766)#774
kasnder merged 1 commit into
masterfrom
fix/wg-rotation-edge-cases

Conversation

@kasnder

@kasnder kasnder commented Aug 22, 2026

Copy link
Copy Markdown
Member

Fixes #766 — the four verified WireGuard rotation/profile-generation edge cases, bundled because each alone is tiny.

1. Provider resources orphaned when the relay fetch fails (the one with real user cost)

MullvadProfileGenerator.java:126-142 created the Mullvad device (fetchWebToken + createDevice), and IvpnProfileGenerator.java:133-146 minted the IVPN session, before chooseRelay(fetchRelays(), …) ran. A failing relay-list fetch therefore left a device/session attached to no profile.

This is the only item with an irreversible external cost: a Mullvad account has a hard cap of 5 device slots and IVPN caps concurrent sessions, so repeated failed setups burn provider quota, and nothing in the app can release the orphan — the user has to clean it up on the provider dashboard.

Fix: fetch relays and choose the relay first; create the device/session only once a relay is secured. Nothing after creation can fail (buildConfig is pure string building), so no cleanup path is introduced — deliberately, since best-effort cleanup here would risk masking the original error for no gain.

2. Write-only previous-key prefs retained old private keys

<provider>_previous_privkey / <provider>_previous_address were written in VpnKeyRotationManager.commitProviderKey (:349-353) and cleared on completion (:429-434), but read nowhererollbackProvider uses its in-memory parameters. They bought no recovery, while process death between the write and the clear left an old WireGuard private key in plaintext SharedPreferences indefinitely.

Fix: drop the writes and the now-dead clearPrevious/currentAddress helpers, and clean up any values already persisted by earlier versions in ApplicationEx.migratePreferences so existing installs don't keep a stale key forever.

Deliberately out of scope: implementing real prefs-backed rollback recovery (surviving process death mid-rotation). That is a much larger design change — it needs a durable rotation journal and a resume path on startup — and does not belong in a bundle of small edge-case fixes.

3. Blind /32 suffix (latent hardening)

VpnKeyRotationManager.java:433-438 appended /32 regardless of address family. The issue missed a second copy at IvpnProfileGenerator.java:317-321; both are fixed to emit /128 for IPv6. Latent today because IVPN currently returns IPv4, so this is correctness hardening rather than a live bug.

4. Malformed Address line for IPv4-less reusable profiles (latent hardening)

MullvadProfileGenerator.buildConfig (:354-357) emitted Address = , fc00:… when ipv4_address was empty — reachable via deviceFromConfig (:196-208) for an imported IPv6-only reusable config. TC's own parser tolerates it; standard wg-quick does not. Now only the non-empty components are joined.

Test evidence

New ProfileGeneratorTest (5 tests) and PreviousKeyPreferenceMigrationTest (1 test). The generators are exercised through small package-private seams (fetchRelays, fetchWebToken, createDevice, createSession, plus newPrivateKey/derivePublicKey so Robolectric never loads the native wgbridge library); production behaviour is unchanged.

  • Item 1: mullvadRelayFailureDoesNotCreateDevice / ivpnRelayFailureDoesNotCreateSession assert the relay failure propagates unchanged and that fetchWebToken/createDevice/createSession were never invoked. Verified this fails on the pre-fix ordering: temporarily restoring the old order in MullvadProfileGenerator.generate gives 5 tests completed, 1 failed.
  • Item 4: mullvadIpv6OnlyReusableConfigHasNoLeadingAddressComma and mullvadReusableConfigKeepsBothAddresses.
  • Item 3: ivpnIpv6AddressGetsIpv6Cidr asserts Address = fc00:bbbb:bbbb:bb01::2/128.
  • Item 2: obsoletePreviousKeyPreferencesAreRemoved seeds all four legacy keys plus unrelated prefs and asserts the migration removes exactly the four. The pre-existing VpnKeyRotationManagerTest assertion on mullvad_previous_privkey still passes.
./gradlew :app:compileGithubDebugJavaWithJavac -q                  # exit 0
./gradlew :app:testGithubDebugUnitTest --tests '*VpnKeyRotation*' \
    --tests '*WgProfile*' --tests '*Ivpn*' --tests '*Mullvad*' -q  # exit 0
./gradlew :app:testGithubDebugUnitTest -q                          # exit 0

Full suite: 262 tests, 0 skipped, 0 failures, 0 errors.

Create provider resources only after a relay is secured, stop
persisting write-only previous-key prefs, pick the CIDR prefix by
address family, and omit the empty address component for IPv4-less
Mullvad configs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kasnder
kasnder merged commit a26d5e4 into master Aug 22, 2026
2 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.

WireGuard rotation edge cases: orphaned devices/sessions, malformed IPv6 Address line, blind /32 suffix, write-only previous-key prefs

1 participant