@@ -153,7 +153,43 @@ final class VPNDaemon: Daemon, DatabaseAccess, ProvidersAccess {
153153 ServiceQualityManager . shared. connectionAttemptEvent ( )
154154 }
155155
156- scheduleFallbackTimerIfNeeded ( )
156+ if fallbackTimer == nil {
157+ log. debug ( " Setting up fallbackTimer... " )
158+
159+ fallbackTimer = Timer . scheduledTimer ( withTimeInterval: Client . configuration. vpnConnectivityRetryDelay, repeats: true ) { [ weak self] timer in
160+ guard let self else { return }
161+ log. debug ( " Executing fallbackTimer... " )
162+
163+ let address = try ? Client . providers. serverProvider. targetServer. bestAddress ( )
164+ address? . markServerAsUnavailable ( )
165+
166+ self . numberOfAttempts += 1
167+ if self . numberOfAttempts < Client . configuration. vpnConnectivityMaxAttempts || self . isReconnectingAfterConnectivityFailure {
168+ log. debug ( " NEVPNManager is still connecting. Reconnecting with a different server... " )
169+ self . updateUIWithAttemptNumber ( self . numberOfAttempts)
170+ self . isReconnecting = true
171+ Client . providers. vpnProvider. reconnect ( after: 0 , forceDisconnect: true ) { error in
172+ if error != nil {
173+ // Reconnect initiation failed — clear flag immediately so the
174+ // subsequent .disconnected status change can clean up normally.
175+ self . isReconnecting = false
176+ }
177+ // On success: leave isReconnecting=true. It will be cleared in
178+ // tryUpdateStatus when .connecting status arrives, ensuring that
179+ // the intermediate .disconnecting → .disconnected transitions do
180+ // not briefly expose vpnStatus = .disconnected to the rest of the app.
181+ }
182+ } else {
183+ log. debug ( " Max number of VPN reconnections. Disconnecting... " )
184+ Client . providers. vpnProvider. disconnect { error in
185+ Macros . postNotification ( . PIAVPNDidFail)
186+ self . reset ( )
187+ self . invalidateTimer ( )
188+ }
189+ }
190+ }
191+
192+ }
157193
158194 case . disconnecting:
159195 nextStatus = . disconnecting
@@ -167,31 +203,7 @@ final class VPNDaemon: Daemon, DatabaseAccess, ProvidersAccess {
167203 return
168204 }
169205
170- // No internet while a connection was already in progress: keep trying
171- // indefinitely instead of giving up. Reaching this point means the previous
172- // status was not .disconnected, so a connection attempt (or an established
173- // connection) was underway. Force the status back to .connecting and keep
174- // the fallback timer alive — recreating it if it was already invalidated —
175- // so we reconnect as soon as the network becomes reachable again.
176- // Skipped when the user disconnected manually.
177- if !accessedDatabase. transient. isNetworkReachable,
178- !Client. configuration. disconnectedManually
179- {
180- log. debug ( " No internet while connecting — staying in .connecting and keeping the retry timer alive " )
181- isReconnecting = true
182- scheduleFallbackTimerIfNeeded ( )
183-
184- accessedDatabase. plain. lastKnownVpnStatus = . connecting
185- if previousStatus != . connecting {
186- accessedDatabase. transient. vpnStatus = . connecting
187- }
188- return
189- }
190-
191- // A manual disconnect must always tear down the retry loop, even mid-reconnect
192- // (isReconnecting == true), otherwise the fallback timer would keep firing and
193- // force the status back to .connecting after the user asked to disconnect.
194- if !isReconnecting || Client . configuration. disconnectedManually {
206+ if !isReconnecting {
195207 invalidateTimer ( )
196208 reset ( )
197209 }
@@ -261,25 +273,19 @@ final class VPNDaemon: Daemon, DatabaseAccess, ProvidersAccess {
261273 }
262274 }
263275
264- log. debug ( " connectivityCheckFailed= \( connectivityCheckFailed) previousStatus= \( previousStatus) " )
276+ log. debug ( " [VPNDaemon] connectivityCheckFailed=\( connectivityCheckFailed) previousStatus= \( previousStatus) " )
265277
266- if disconnectedManually {
267- // The user asked to disconnect — never reconnect on their behalf,
268- // whatever error the tunnel died with.
269- log. debug ( " Manual disconnect — ignoring last disconnect error " )
270- } else if connectivityCheckFailed {
271- let wholeInternetIsReachable = accessedDatabase. transient. isNetworkReachable
272- if wholeInternetIsReachable, let lastConnectedCN = accessedDatabase. plain. lastServerCN {
273- log. debug ( " connectivityCheckFailed — marking current server as unavailable and triggering reconnect " )
278+ if connectivityCheckFailed {
279+ log. debug ( " [VPNDaemon] connectivityCheckFailed — marking current server as unavailable and triggering reconnect " )
280+
281+ if let lastConnectedCN = accessedDatabase. plain. lastServerCN {
274282 let targetRegion = try ? Client . providers. serverProvider. targetServer
275283 let lastConnectedServer = targetRegion? . addresses ( ) . first ( where: { $0. cn == lastConnectedCN } )
276284 lastConnectedServer? . markServerAsUnavailable ( )
277- } else if !wholeInternetIsReachable {
278- log. debug ( " There's no internet! " )
279285 }
280286
281287 isReconnectingAfterConnectivityFailure = true
282- Client . providers. vpnProvider. reconnect ( forceDisconnect: true , nil )
288+ Client . providers. vpnProvider. reconnect ( after : nil , forceDisconnect: true , nil )
283289 } else {
284290 if previousStatus == . connecting {
285291 log. error ( " The VPN did fail \( lastDisconnectError) " )
@@ -291,72 +297,6 @@ final class VPNDaemon: Daemon, DatabaseAccess, ProvidersAccess {
291297 }
292298 }
293299
294- // MARK: Fallback timer
295-
296- /// Schedules the repeating reconnection timer if it is not already running.
297- /// The timer fires every `vpnConnectivityRetryDelay` seconds, marking the current
298- /// server as unavailable and attempting a reconnect. It keeps retrying while there
299- /// are attempts left, while there is no internet, or while recovering from a
300- /// connectivity failure; otherwise it gives up and disconnects.
301- private func scheduleFallbackTimerIfNeeded( ) {
302- guard fallbackTimer == nil else { return }
303- log. debug ( " Setting up fallbackTimer... " )
304-
305- fallbackTimer = Timer . scheduledTimer ( withTimeInterval: Client . configuration. vpnConnectivityRetryDelay, repeats: true ) { [ weak self] timer in
306- guard let self else { return }
307- log. debug ( " Executing fallbackTimer... " )
308-
309- // The user disconnected manually: stop retrying. Without this check
310- // the timer keeps reconnecting when no NEVPNStatusDidChange arrives
311- // to tear it down (e.g. the tunnel was already dead when the user
312- // tapped disconnect during a network change).
313- guard !Client. configuration. disconnectedManually else {
314- log. debug ( " Manual disconnect — stopping the reconnection retry timer " )
315- self . invalidateTimer ( )
316- self . reset ( )
317- return
318- }
319-
320- let address = try ? Client . providers. serverProvider. targetServer. bestAddress ( )
321- address? . markServerAsUnavailable ( )
322-
323- self . numberOfAttempts += 1
324-
325- let shouldKeepTrying =
326- numberOfAttempts < Client . configuration. vpnConnectivityMaxAttempts
327- || !accessedDatabase. transient. isNetworkReachable
328- || isReconnectingAfterConnectivityFailure
329-
330- if shouldKeepTrying {
331- log. debug ( " NEVPNManager is still connecting. Reconnecting with a different server... " )
332- self . updateUIWithAttemptNumber ( self . numberOfAttempts)
333- self . isReconnecting = true
334- Client . providers. vpnProvider. reconnect ( forceDisconnect: true ) { error in
335- if let clientError = error as? ClientError , clientError == . internetUnreachable {
336- self . isReconnecting = true
337- self . isReconnectingAfterConnectivityFailure = true
338-
339- } else if error != nil {
340- // Reconnect initiation failed — clear flag immediately so the
341- // subsequent .disconnected status change can clean up normally.
342- self . isReconnecting = false
343- }
344- // On success: leave isReconnecting=true. It will be cleared in
345- // tryUpdateStatus when .connecting status arrives, ensuring that
346- // the intermediate .disconnecting → .disconnected transitions do
347- // not briefly expose vpnStatus = .disconnected to the rest of the app.
348- }
349- } else {
350- log. debug ( " Max number of VPN reconnections. Disconnecting... " )
351- Client . providers. vpnProvider. disconnect { error in
352- Macros . postNotification ( . PIAVPNDidFail)
353- self . reset ( )
354- self . invalidateTimer ( )
355- }
356- }
357- }
358- }
359-
360300 // MARK: Invalidate
361301 private func invalidateTimer( ) {
362302 fallbackTimer? . invalidate ( )
0 commit comments