Skip to content

Commit 0ee32f4

Browse files
committed
fix: resolve race conditions and crash on VPN disconnect
- Reversed stop order: cancel Go context first, wait for client to drain, then stop tun2socks engines - Introduced clientDone channel in Go to synchronize client shutdown with engine teardown - Handled JNI CancellationException and 'context canceled' logs safely in Kotlin service - Prevented double-close native SIGSEGV crash using duplicated TUN fd
1 parent 9603d19 commit 0ee32f4

2 files changed

Lines changed: 45 additions & 11 deletions

File tree

android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,18 @@ class MasterDnsVpnService : VpnService() {
222222
configFile.absolutePath,
223223
logFile.absolutePath
224224
)
225+
} catch (_: CancellationException) {
226+
// Normal shutdown — coroutine was cancelled during disconnect.
227+
VpnManager.appendLog("Go core stopped (coroutine cancelled)")
225228
} catch (e: Exception) {
226-
Log.e(TAG, "Go core error", e)
227-
VpnManager.appendLog("Go core error: ${e.message}")
228-
withContext(Dispatchers.Main) {
229-
VpnManager.setError("Go core error: ${e.message}")
229+
// Only log real errors, not context cancellation from Go.
230+
val msg = e.message ?: ""
231+
if (!msg.contains("context canceled", ignoreCase = true)) {
232+
Log.e(TAG, "Go core error", e)
233+
VpnManager.appendLog("Go core error: $msg")
234+
runCatching {
235+
VpnManager.setError("Go core error: $msg")
236+
}
230237
}
231238
}
232239
}

mobile/mobile.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"path/filepath"
1414
"sync"
1515
"syscall"
16+
"time"
1617

1718
"masterdnsvpn-go/internal/client"
1819
"masterdnsvpn-go/internal/config"
@@ -35,6 +36,7 @@ var (
3536
logCb LogCallback
3637
tunRunning bool
3738
tunBridgeRunning bool
39+
clientDone chan struct{} // closed when app.Run(ctx) returns
3840
)
3941

4042
// Bandwidth holds upload and download counters for gomobile bindings.
@@ -70,19 +72,26 @@ func StartClient(configPath string, logPath string) error {
7072
}
7173

7274
ctx, cancel := context.WithCancel(context.Background())
75+
done := make(chan struct{})
7376

7477
mu.Lock()
7578
vpnClient = app
7679
cancelFunc = cancel
80+
clientDone = done
7781
running = true
7882
mu.Unlock()
7983

8084
runErr := app.Run(ctx)
8185

86+
// Signal that the client has fully stopped BEFORE clearing state.
87+
// StopClient waits on this channel to know it's safe to tear down tun2socks.
88+
close(done)
89+
8290
mu.Lock()
8391
running = false
8492
vpnClient = nil
8593
cancelFunc = nil
94+
clientDone = nil
8695
mu.Unlock()
8796

8897
return runErr
@@ -139,18 +148,36 @@ func StopTun() {
139148
}
140149

141150
// StopClient gracefully stops the running client.
151+
// Order: cancel Go context first (stops traffic), wait for client to drain,
152+
// THEN stop tun2socks (no more goroutines writing to the engine).
142153
func StopClient() {
143-
StopTun()
144-
StopTunBridge()
145-
154+
// 1. Cancel context — tells app.Run(ctx) to shut down.
146155
mu.Lock()
147-
defer mu.Unlock()
148-
if cancelFunc != nil {
149-
cancelFunc()
150-
cancelFunc = nil
156+
cancel := cancelFunc
157+
done := clientDone
158+
cancelFunc = nil
159+
mu.Unlock()
160+
161+
if cancel != nil {
162+
cancel()
163+
}
164+
165+
// 2. Wait for the Go client to fully stop (with timeout).
166+
// This ensures no goroutine is writing to the tun2socks engine
167+
// when we tear it down in step 3.
168+
if done != nil {
169+
select {
170+
case <-done:
171+
case <-time.After(3 * time.Second):
172+
}
151173
}
174+
175+
// 3. NOW it's safe to stop tun2socks — no traffic flowing.
176+
StopTun()
177+
StopTunBridge()
152178
}
153179

180+
154181
// IsRunning returns true if the client is currently running.
155182
func IsRunning() bool {
156183
mu.Lock()

0 commit comments

Comments
 (0)