Skip to content

Fix: duplicate connect toggle tears down the winning VPN attempt - #8959

Draft
david-lant wants to merge 1 commit into
mainfrom
auto-fix/ticket-181166
Draft

Fix: duplicate connect toggle tears down the winning VPN attempt#8959
david-lant wants to merge 1 commit into
mainfrom
auto-fix/ticket-181166

Conversation

@david-lant

Copy link
Copy Markdown

Summary

Auto-fix for Freshdesk ticket #181166 (Android 9.1.18, China / AS4134, Pro user: "打开软件跟没打开一样始终连接不上").

Root cause: Methods.ConnectToServer posts no VPNStatus.Connecting, so the duplicate connect it lets through is rejected in a way that tears down the network listener owned by the attempt that is still running.

Chain, as observed in the ticket's logs (36 occurrences over the capture; the reported session at 03:44:22.61203:44:27.068):

  1. MethodHandler.kt Methods.ConnectToServer dispatched without posting VPNStatus.Connecting, unlike its sibling Methods.Start. The only debounce for the toggle is the Dart guard in lib/features/vpn/provider/vpn_notifier.dart, which returns early only on connecting / disconnecting — so the state stayed disconnected for the entire ~4.4 s connect and a second tap 1.48 s later dispatched a duplicate ACTION_CONNECT_TO_SERVER.
  2. The duplicate hit the connectInFlight single-flight gate in LanternVpnService.launchVPN, which rejected it by throwing into the shared runCatching { }.onFailure { }. That failure path belongs to the in-flight, ultimately successful attempt, so the rejected attempt: posted VPNStatus.Error (UI showed Error 202 ms before the tunnel reported Connected), called DefaultNetworkMonitor.setNetworkChangeCallback(null) + stop(), and — for the start_vpn variant, which sets cleanUpOnFailure = true — ran serviceCleanUp(), unregistering the VPN status receiver.
  3. DefaultNetworkListener.listeners is keyed by the DefaultNetworkMonitor singleton, so both attempts share one entry: the loser's stop() unregisters the connectivity callback outright. The winner never re-registers, so updateUnderlyingNetworks() stops firing on Wi-Fi/cellular roam — the tunnel reports Connected while outbound dials fail (14,611 dial wlan0 (42) / rmnet0 (8): i/o timeout in the capture, only 25 updated default interface events over three days).

What changed

android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt

  • Methods.ConnectToServer now posts VPNStatus.Connecting before dispatching and resets to VPNStatus.Disconnected in onFailure, matching Methods.Start. This arms the Dart guard for the whole connect so the duplicate is never dispatched.

android/app/src/main/kotlin/org/getlantern/lantern/service/LanternVpnService.kt

  • The connectInFlight check moves ahead of the DefaultNetworkMonitor setup and exits with return@withContext instead of throw, so a rejected attempt performs no teardown and posts no status. It stays after showStartingVPNConnectedNotification() because the OS still expects a startForeground() for the duplicate startForegroundService().
  • Because the flag is now claimed before setupRadiance() / the monitor setup, onFailure releases it when the connect coroutine was never created (connectLaunched), so a failure in that window can't wedge every later attempt.

Diagnosis was written against v9.1.18-beta (dcd5bbb1b); the change is rebased onto current main, where launchVPN has since moved the foreground-notification call inside runCatching.

Test plan

  • Cold-start with radiance not yet initialized (so setupRadiance() runs inside launchVPN), select a specific (non-auto) server so the Dart path is connectToServer, tap the toggle twice ~1.5 s apart. Expect: one ACTION_CONNECT_TO_SERVER dispatch, second tap filtered by the Dart guard, no previous VPN connect attempt still in flight, no Error before Connected.
  • With the Dart guard bypassed (or via a rapid double intent), confirm the rejected attempt logs Ignoring VPN operation (...), leaves DefaultNetworkListener registered, and posts no status.
  • Force a failure inside setupRadiance() and confirm a subsequent connect attempt still runs (flag released).
  • Toggle connect/disconnect repeatedly and verify no state gets stuck in connecting (VPN consent denial should still land on MissingPermission).
  • Wi-Fi ⇄ cellular roam while connected: updated default interface continues to fire.

Auto-generated by /issue-fix from an interactive /ticket-autodiagnose run.

Auto-diagnosed from Freshdesk ticket #181166.

The ConnectToServer method-channel branch never posted VPNStatus.Connecting,
so the Dart toggle guard (vpn_notifier.dart, which only debounces on
connecting/disconnecting) stayed un-armed for the whole multi-second connect
and a second tap dispatched a duplicate connect. The duplicate was then
rejected by connectInFlight via a throw into launchVPN's shared failure
handler, so the losing attempt unregistered DefaultNetworkMonitor (a process
singleton, so it deregisters the listener outright) and posted VPNStatus.Error
for the attempt that was about to succeed — leaving the tunnel up but the UI in
error and sing-box bound to a stale interface across Wi-Fi/cellular roams.

Post Connecting before dispatch (and reset to Disconnected on failure) to match
the sibling Methods.Start branch, and make the in-flight rejection an early
return placed before the network-monitor setup so a rejected attempt tears down
nothing and posts no status.

Co-Authored-By: Claude <noreply@anthropic.com>
@david-lant david-lant added the auto-fix Automated fix from ticket diagnosis label Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 180ba2bb-577a-4410-a42f-fcfd5c808a5f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-fix Automated fix from ticket diagnosis

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant