Skip to content

Commit 4e26069

Browse files
author
Andre Fonseca
committed
Merge branch 'crash_fix' into 'main'
Crash fix See merge request ws/client/iosapp!1230
2 parents 8f3a15a + 5d7c36a commit 4e26069

File tree

4 files changed

+81
-39
lines changed

4 files changed

+81
-39
lines changed

PacketTunnel/PacketTunnelProvider.swift

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelCredentialsManaging {
5555
// Check if tunnel was force disconnected (e.g., due to session invalidation)
5656
// This mirrors WireGuard's credential check but uses a flag since OpenVPN
5757
// credentials are embedded in tunnel config and can't be deleted
58-
if preferences.getDisconnectReason() != DisconnectReason.unknown {
59-
consoleLogger.debug("Tunnel start blocked - force disconnect flag is set")
60-
logger.logI("PacketTunnelProvider", "Blocked tunnel start - force disconnect active", flushImmediately: true)
61-
let error = NSError(domain: "com.windscribe", code: 50, userInfo: [
62-
NSLocalizedDescriptionKey: "Session is invalid"
63-
])
64-
completionHandler(error)
65-
}
6658

6759
guard
6860
let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
@@ -190,25 +182,35 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelCredentialsManaging {
190182
}
191183

192184
override func sleep(completionHandler: @escaping () -> Void) {
185+
logger.logI("PacketTunnelProvider", "Device going to sleep.", flushImmediately: true)
193186
completionHandler()
194187
}
195188

196189
override func wake() {
197-
consoleLogger.debug("Device wake up.")
198-
controlPlane?.checkTunnelHealth()
190+
let currentTime = Date().timeIntervalSince1970
191+
let lastWakeTime = preferences.getWireguardWakeupTime()
192+
logger.logI("PacketTunnelProvider", "Device wakeup.", flushImmediately: true)
193+
if lastWakeTime == 0 || currentTime - lastWakeTime >= 600 {
194+
UserDefaults.standard.set(currentTime, forKey: "lastWakeTime")
195+
preferences.saveWireguardWakeupTime(value: currentTime)
196+
if preferences.getDisconnectReason() == DisconnectReason.unknown {
197+
controlPlane?.checkTunnelHealth()
198+
}
199+
}
199200
}
200201

201202
// MARK: - TunnelCredentialsManaging
202203

204+
@MainActor
203205
func deleteCredentials(error: NSError) {
204206
guard !isCancelling else {
205207
logger.logI("PacketTunnelProvider", "Already cancelling tunnel, ignoring duplicate call.", flushImmediately: true)
206208
return
207209
}
210+
controlPlane = nil
208211
isCancelling = true
209212
// OpenVPN credentials are passed per-connection, not stored persistently
210213
// Log the action for visibility
211-
consoleLogger.debug("Credentials invalidated for OpenVPN tunnel.")
212214
logger.logI("PacketTunnelProvider", "OpenVPN credentials invalidated.", flushImmediately: true)
213215
if vpnReachability.isTracking {
214216
vpnReachability.stopTracking()
@@ -245,10 +247,22 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
245247
self.startHandler = nil
246248
// Initialize control plane when tunnel is ready
247249
if controlPlane == nil {
248-
controlPlane = ControlPlane(apiUtil: apiUtil,api: api,preferences: preferences,credentialsManager:self, tunnelProvider: self, consoleLogger: self.consoleLogger)
250+
controlPlane = ControlPlane(
251+
apiUtil: apiUtil,
252+
api: api,
253+
preferences: preferences,
254+
consoleLogger: self.consoleLogger,
255+
onTunnelShouldStop: { @MainActor [weak self] reason, error in
256+
guard let self = self else { return }
257+
self.logger.logI("PacketTunnelProvider", "Control plane requested tunnel stop for reason: \(reason.rawValue)", flushImmediately: true)
258+
self.deleteCredentials(error: error)
259+
}
260+
)
261+
}
262+
if preferences.getDisconnectReason() == DisconnectReason.unknown {
263+
consoleLogger.debug("Tunnel connected, checking tunnel health.")
264+
controlPlane?.checkTunnelHealth()
249265
}
250-
consoleLogger.debug("Tunnel connected, checking tunnel health.")
251-
controlPlane?.checkTunnelHealth()
252266
case .disconnected:
253267
guard let stopHandler = stopHandler else { return }
254268
if vpnReachability.isTracking {
@@ -258,8 +272,10 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
258272
self.stopHandler = nil
259273
case .reconnecting:
260274
reasserting = true
261-
consoleLogger.debug("Reconnecting, checking tunnel health.")
262-
controlPlane?.checkTunnelHealth()
275+
if preferences.getDisconnectReason() == DisconnectReason.unknown {
276+
logger.logI("PacketTunnelProvider", "Reconnecting, checking tunnel health.", flushImmediately: true)
277+
controlPlane?.checkTunnelHealth()
278+
}
263279
default:
264280
break
265281
}
@@ -277,12 +293,15 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
277293
startHandler(error)
278294
self.startHandler = nil
279295
} else {
280-
guard !isCancelling else {
281-
logger.logI("PacketTunnelProvider", "Already cancelling tunnel, ignoring error handler call.", flushImmediately: true)
282-
return
296+
DispatchQueue.main.async { [weak self] in
297+
guard let self = self else { return }
298+
guard !self.isCancelling else {
299+
self.logger.logI("PacketTunnelProvider", "Already cancelling tunnel, ignoring error handler call.", flushImmediately: true)
300+
return
301+
}
302+
self.isCancelling = true
303+
self.cancelTunnelWithError(error)
283304
}
284-
isCancelling = true
285-
cancelTunnelWithError(error)
286305
}
287306
}
288307
}

Windscribe/Managers/VPN/ControlPlane.swift

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,44 @@ class ControlPlane {
1818
private let apiUtil: APIUtilService
1919
private let api: WSNetServerAPI
2020
private let preferences: Preferences
21+
private let onTunnelShouldStop: @MainActor (DisconnectReason, NSError) -> Void
2122

2223
private var runningHealthCheck = false
23-
private weak var credentialsManager: TunnelCredentialsManaging?
24-
private weak var tunnelProvider: NEPacketTunnelProvider?
24+
private var lastHealthCheckTime: TimeInterval = 0
2525

2626
init(
2727
apiUtil: APIUtilService,
2828
api: WSNetServerAPI,
2929
preferences: Preferences,
30-
credentialsManager: TunnelCredentialsManaging?,
31-
tunnelProvider: NEPacketTunnelProvider,
32-
consoleLogger: Logger
30+
consoleLogger: Logger,
31+
onTunnelShouldStop: @escaping @MainActor (DisconnectReason, NSError) -> Void
3332
) {
3433
self.apiUtil = apiUtil
3534
self.api = api
3635
self.preferences = preferences
37-
self.credentialsManager = credentialsManager
38-
self.tunnelProvider = tunnelProvider
3936
self.consoleLogger = consoleLogger
37+
self.onTunnelShouldStop = onTunnelShouldStop
4038
}
4139

4240
/// Called from packet tunnel provider when tunnel health is not good (handshake fails, wake up, etc.)
4341
func checkTunnelHealth() {
4442
// Skip health check for custom configs
4543
guard !preferences.isCustomConfigSelected() else { return }
46-
guard !runningHealthCheck else { return }
44+
guard !runningHealthCheck else {
45+
consoleLogger.debug("Health check already running, skipping.")
46+
return
47+
}
48+
49+
// Throttle health checks to once every 10 seconds
50+
let currentTime = Date().timeIntervalSince1970
51+
if lastHealthCheckTime > 0 && currentTime - lastHealthCheckTime < 10 {
52+
consoleLogger.debug("Health check throttled (last check was \(Int(currentTime - self.lastHealthCheckTime))s ago), skipping.")
53+
return
54+
}
55+
56+
// Set flag BEFORE dispatching to prevent race condition
57+
runningHealthCheck = true
58+
lastHealthCheckTime = currentTime
4759

4860
DispatchQueue.global().async { [weak self] in
4961
self?.performHealthCheck()
@@ -52,7 +64,6 @@ class ControlPlane {
5264

5365
/// Performs the actual health check by fetching session
5466
private func performHealthCheck() {
55-
runningHealthCheck = true
5667
consoleLogger.debug("Requesting user session update.")
5768

5869
Task { [weak self] in
@@ -104,7 +115,9 @@ class ControlPlane {
104115
let error = NSError(domain: "com.windscribe", code: 50, userInfo: [
105116
NSLocalizedDescriptionKey: reason
106117
])
107-
credentialsManager?.deleteCredentials(error: error)
118+
119+
// Call the callback on main thread to handle tunnel cancellation
120+
await onTunnelShouldStop(reason, error)
108121
}
109122

110123
func getSession() async throws -> Session {

Windscribe/Managers/VPN/TunnelCredentialsManaging.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import Foundation
1010

1111
/// Protocol for tunnel providers to manage their credentials
12+
/// Methods must be called on the main thread as they interact with NetworkExtension APIs
1213
public protocol TunnelCredentialsManaging: AnyObject {
13-
func deleteCredentials(error: NSError)
14+
@MainActor func deleteCredentials(error: NSError)
1415
}

WireGuardTunnel/PacketTunnelProvider.swift

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelCredentialsManaging {
4848

4949
private lazy var adapter: WireGuardAdapter = .init(with: self) { _, message in
5050
if message.contains("Retrying handshake") {
51-
self.controlPlane?.checkTunnelHealth()
51+
if self.preferences.getDisconnectReason() == DisconnectReason.unknown {
52+
self.controlPlane?.checkTunnelHealth()
53+
}
5254
}
5355
}
5456

@@ -118,9 +120,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelCredentialsManaging {
118120
apiUtil: self.apiUtil,
119121
api: self.api,
120122
preferences: self.preferences,
121-
credentialsManager: self,
122-
tunnelProvider: self,
123-
consoleLogger: self.consoleLogger
123+
consoleLogger: self.consoleLogger,
124+
onTunnelShouldStop: { @MainActor [weak self] reason, error in
125+
guard let self = self else { return }
126+
self.logger.logI("PacketTunnelProvider", "Control plane requested tunnel stop for reason: \(reason.rawValue)", flushImmediately: true)
127+
self.deleteCredentials(error: error)
128+
}
124129
)
125130

126131

@@ -155,8 +160,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelCredentialsManaging {
155160
completionHandler(PacketTunnelProviderError.couldNotStartBackend)
156161

157162
case .invalidState:
158-
// Must never happen
159-
fatalError()
163+
completionHandler(PacketTunnelProviderError.couldNotStartBackend)
160164
}
161165
}
162166
}
@@ -203,21 +207,26 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelCredentialsManaging {
203207
logger.logI("PacketTunnelProvider", "Device wake up.", flushImmediately: true)
204208
UserDefaults.standard.set(currentTime, forKey: "lastWakeTime")
205209
preferences.saveWireguardWakeupTime(value: currentTime)
206-
controlPlane?.checkTunnelHealth()
210+
if preferences.getDisconnectReason() == DisconnectReason.unknown {
211+
controlPlane?.checkTunnelHealth()
212+
}
207213
}
208214
}
209215

210216
override func sleep(completionHandler: @escaping () -> Void) {
217+
logger.logI("PacketTunnelProvider", "Device going to sleep.", flushImmediately: true)
211218
completionHandler()
212219
}
213220

214221
// MARK: - TunnelCredentialsManaging
215222

223+
@MainActor
216224
func deleteCredentials(error: NSError) {
217225
guard !isCancelling else {
218226
logger.logI("PacketTunnelProvider", "Already cancelling tunnel, ignoring duplicate call.", flushImmediately: true)
219227
return
220228
}
229+
controlPlane = nil
221230
isCancelling = true
222231
wgCrendentials.delete()
223232
logger.logI("PacketTunnelProvider", "Deleted WireGuard credentials.", flushImmediately: true)

0 commit comments

Comments
 (0)