Skip to content

fix(vpn): close stale vpnInterface on reconnect and throw on go-core … - #41

Merged
Hidden-Node merged 1 commit into
mainfrom
advisor/013-tun-fd-leak-reconnect
Jul 23, 2026
Merged

fix(vpn): close stale vpnInterface on reconnect and throw on go-core …#41
Hidden-Node merged 1 commit into
mainfrom
advisor/013-tun-fd-leak-reconnect

Conversation

@Hidden-Node

Copy link
Copy Markdown
Owner

…stop timeout

Reconnecting while the prior Go core hadn't fully stopped leaked the old ParcelFileDescriptor: ensureGoCoreStopped() only waited 4s, logged a warning, and continued, so the next builder.establish() overwrote vpnInterface at line 360 without closing the previous one. After enough reconnects the process hit its fd soft limit and establish() threw.

Two-part fix:

  1. Add closeStaleVpnInterface() and call it at the top of startVpn() AFTER ensureGoCoreStopped(). The ordering preserves the existing invariant (see MasterDnsVpnService.kt:491 comment): the fd is closed only once the Go core has stopped, so tun2socks goroutines mid-read on the fd don't hit EBADF. Also clears tunBridgeActive so a later stopClient() doesn't assume a live bridge on the stale fd.

  2. Make ensureGoCoreStopped() throw IllegalStateException on timeout instead of logging-and-continuing. The throw propagates to the catch-all in startVpn(), which calls setError(message) and runs the proven stopVpn() cleanup path (Mobile.stopClient is Go-side idempotent; stopVpn itself is wrapped in try/catch). The user now sees a "Go core did not stop cleanly within 4 seconds" error instead of a silent CONNECTING-forever or cascading fd leaks.

No recursion risk: stopVpn() does not call startVpn() or ensureGoCoreStopped() (grep-confirmed single call site at line 150).

Scope: MasterDnsVpnService.kt only; no Go bridge, go.mod, or go.sum changes. Composes with plan 014 (isStopping lifecycle) cleanly.

Verification: gradle build skipped per no-local-build constraint; CI on push is the gate. Grep-based done criteria pass:

  • closeStaleVpnInterface() x2 matches (def + call site)
  • throw "Go core did not stop cleanly within 4 seconds" present
  • "Warning: Go core may still be running" removed (0 matches)

…stop timeout

Reconnecting while the prior Go core hadn't fully stopped leaked the old
ParcelFileDescriptor: ensureGoCoreStopped() only waited 4s, logged a
warning, and continued, so the next builder.establish() overwrote
vpnInterface at line 360 without closing the previous one. After enough
reconnects the process hit its fd soft limit and establish() threw.

Two-part fix:

1. Add closeStaleVpnInterface() and call it at the top of startVpn()
   AFTER ensureGoCoreStopped(). The ordering preserves the existing
   invariant (see MasterDnsVpnService.kt:491 comment): the fd is closed
   only once the Go core has stopped, so tun2socks goroutines mid-read
   on the fd don't hit EBADF. Also clears tunBridgeActive so a later
   stopClient() doesn't assume a live bridge on the stale fd.

2. Make ensureGoCoreStopped() throw IllegalStateException on timeout
   instead of logging-and-continuing. The throw propagates to the
   catch-all in startVpn(), which calls setError(message) and runs the
   proven stopVpn() cleanup path (Mobile.stopClient is Go-side
   idempotent; stopVpn itself is wrapped in try/catch). The user now
   sees a "Go core did not stop cleanly within 4 seconds" error instead
   of a silent CONNECTING-forever or cascading fd leaks.

No recursion risk: stopVpn() does not call startVpn() or
ensureGoCoreStopped() (grep-confirmed single call site at line 150).

Scope: MasterDnsVpnService.kt only; no Go bridge, go.mod, or go.sum
changes. Composes with plan 014 (isStopping lifecycle) cleanly.

Verification: gradle build skipped per no-local-build constraint;
CI on push is the gate. Grep-based done criteria pass:
- closeStaleVpnInterface() x2 matches (def + call site)
- throw "Go core did not stop cleanly within 4 seconds" present
- "Warning: Go core may still be running" removed (0 matches)
@Hidden-Node
Hidden-Node merged commit 6b2c7af into main Jul 23, 2026
2 checks passed
@Hidden-Node
Hidden-Node deleted the advisor/013-tun-fd-leak-reconnect branch July 23, 2026 20:38
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.

1 participant