diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-03-03 12:02:27 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-03-03 12:02:27 +0100 |
| commit | 4d37537a6ea57d40d891df7ae65272a97bc3e927 (patch) | |
| tree | ac4563389a13f7767da2867419ebdca221e43bb6 | |
| parent | 639e2da07be3be0a638379beb6cfd399ddb0c176 (diff) | |
| parent | b17e032c45e6ec4577b8a8e143b9d82715ccfd90 (diff) | |
| download | mullvadvpn-4d37537a6ea57d40d891df7ae65272a97bc3e927.tar.xz mullvadvpn-4d37537a6ea57d40d891df7ae65272a97bc3e927.zip | |
Merge branch 'serialize-reconnect-ios'
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelProvider.swift | 96 | ||||
| -rw-r--r-- | ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift | 200 | ||||
| -rw-r--r-- | ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift | 5 |
3 files changed, 180 insertions, 121 deletions
diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 8b59185b90..4a7e10e471 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -38,6 +38,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { /// completion handler passed into `startTunnel`. private var isConnected = false + /// Raised once tunnel receives the first call to `stopTunnel()`. + /// Once this happens all requests to reconnect the tunnel will be ignored. + private var isStopping = false + /// Flag indicating whether network is reachable. private var isNetworkReachable = true @@ -83,6 +87,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { /// Last device check task. private var checkDeviceStateTask: Cancellable? + /// Last task to reconnect the tunnel. + private var reconnectTunnelTask: Operation? + /// Internal operation queue. private let operationQueue = AsyncOperationQueue() @@ -153,7 +160,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { } ) - tunnelMonitor = TunnelMonitor(queue: dispatchQueue, adapter: adapter) + tunnelMonitor = TunnelMonitor(delegateQueue: dispatchQueue, adapter: adapter) tunnelMonitor.delegate = self } @@ -207,7 +214,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { configurationError = error startEmptyTunnel(completionHandler: completionHandler) - beginTunnelStartupFailureRecovery() + dispatchQueue.async { + self.beginTunnelStartupFailureRecovery() + } return } @@ -251,24 +260,30 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { providerLogger.debug("Stop the tunnel: \(reason)") dispatchQueue.async { + self.isStopping = true self.cancelTunnelStartupFailureRecovery() - self.tunnelMonitor.stop() - self.checkDeviceStateTask?.cancel() - self.checkDeviceStateTask = nil self.startTunnelCompletionHandler = nil } - adapter.stop { error in - self.dispatchQueue.async { - if let error = error { - self.providerLogger.error( - error: error, - message: "Failed to stop the tunnel gracefully." - ) - } else { - self.providerLogger.debug("Stopped the tunnel.") + // Cancel all operations: reconnection requests, network requests. + operationQueue.cancelAllOperations() + + // Stop tunnel monitor after all operations are kicked off the queue. + operationQueue.addBarrierBlock { + self.tunnelMonitor.stop() + + self.adapter.stop { error in + self.dispatchQueue.async { + if let error = error { + self.providerLogger.error( + error: error, + message: "Failed to stop the tunnel gracefully." + ) + } else { + self.providerLogger.debug("Stopped the tunnel.") + } + completionHandler() } - completionHandler() } } } @@ -297,7 +312,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { let nextRelay: NextRelay = (appSelectorResult ?? self.selectorResult) .map { .set($0) } ?? .automatic - self.reconnectTunnel(to: nextRelay) + self.reconnectTunnel(to: nextRelay, shouldStopTunnelMonitor: true) completionHandler?(nil) @@ -365,10 +380,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { setReconnecting(false) } - func tunnelMonitorDelegate( - _ tunnelMonitor: TunnelMonitor, - shouldHandleConnectionRecoveryWithCompletion completionHandler: @escaping () -> Void - ) { + func tunnelMonitorDelegateShouldHandleConnectionRecovery(_ tunnelMonitor: TunnelMonitor) { dispatchPrecondition(condition: .onQueue(dispatchQueue)) let (value, isOverflow) = numberOfFailedAttempts.addingReportingOverflow(1) @@ -380,9 +392,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { providerLogger.debug("Recover connection. Picking next relay...") - reconnectTunnel(to: .automatic) { _ in - completionHandler() - } + reconnectTunnel(to: .automatic, shouldStopTunnelMonitor: false) } func tunnelMonitor( @@ -402,12 +412,17 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { // MARK: - Private private func beginTunnelStartupFailureRecovery() { + dispatchPrecondition(condition: .onQueue(dispatchQueue)) + let timer = DispatchSource.makeTimerSource(queue: dispatchQueue) timer.setEventHandler { [weak self] in guard let self = self else { return } self.providerLogger.debug("Restart the tunnel that had startup failure.") - self.reconnectTunnel(to: .automatic) { [weak self] error in + self.reconnectTunnel( + to: .automatic, + shouldStopTunnelMonitor: false + ) { [weak self] error in if error == nil { self?.cancelTunnelStartupFailureRecovery() } @@ -425,6 +440,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { } private func cancelTunnelStartupFailureRecovery() { + dispatchPrecondition(condition: .onQueue(dispatchQueue)) + tunnelStartupFailureRecoveryTimer?.cancel() tunnelStartupFailureRecoveryTimer = nil } @@ -491,6 +508,37 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { private func reconnectTunnel( to nextRelay: NextRelay, + shouldStopTunnelMonitor: Bool, + completionHandler: ((Error?) -> Void)? = nil + ) { + dispatchPrecondition(condition: .onQueue(dispatchQueue)) + + // Ignore all requests to reconnect once tunnel is preparing to stop. + guard !isStopping else { return } + + let blockOperation = AsyncBlockOperation(dispatchQueue: dispatchQueue) { operation in + if shouldStopTunnelMonitor { + self.tunnelMonitor.stop() + } + + self.reconnectTunnelInner(to: nextRelay) { error in + completionHandler?(error) + operation.finish() + } + } + + if let reconnectTunnelTask = reconnectTunnelTask { + blockOperation.addDependency(reconnectTunnelTask) + } + + reconnectTunnelTask?.cancel() + reconnectTunnelTask = blockOperation + + operationQueue.addOperation(blockOperation) + } + + private func reconnectTunnelInner( + to nextRelay: NextRelay, completionHandler: ((Error?) -> Void)? = nil ) { dispatchPrecondition(condition: .onQueue(dispatchQueue)) diff --git a/ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift b/ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift index 43daed87eb..d1ceddad40 100644 --- a/ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift +++ b/ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift @@ -64,6 +64,10 @@ final class TunnelMonitor: PingerDelegate { /// Connection is established. case connected + /// Delegate is recovering connection. + /// Delegate has to call `start(probeAddress:)` to complete recovery and resume monitoring. + case recovering + /// Waiting for network connectivity. case waitingConnectivity } @@ -171,7 +175,7 @@ final class TunnelMonitor: PingerDelegate { return maxEstablishTimeout } - case .pendingStart, .connected, .waitingConnectivity, .stopped: + case .pendingStart, .connected, .waitingConnectivity, .stopped, .recovering: return pingTimeout } } @@ -218,7 +222,9 @@ final class TunnelMonitor: PingerDelegate { } private let adapter: WireGuardAdapter - private let internalQueue = DispatchQueue(label: "TunnelMonitor") + + private let nslock = NSLock() + private let eventQueue = DispatchQueue(label: "TunnelMonitor-eventQueue") private let delegateQueue: DispatchQueue private let pinger: Pinger @@ -233,52 +239,82 @@ final class TunnelMonitor: PingerDelegate { private weak var _delegate: TunnelMonitorDelegate? weak var delegate: TunnelMonitorDelegate? { set { - internalQueue.sync { - _delegate = newValue - } + nslock.lock() + defer { nslock.unlock() } + + _delegate = newValue } get { - return internalQueue.sync { - return _delegate - } + nslock.lock() + defer { nslock.unlock() } + + return _delegate } } - init(queue: DispatchQueue, adapter anAdapter: WireGuardAdapter) { - delegateQueue = queue + init(delegateQueue: DispatchQueue, adapter anAdapter: WireGuardAdapter) { + self.delegateQueue = delegateQueue adapter = anAdapter - pinger = Pinger(delegateQueue: internalQueue) + pinger = Pinger(delegateQueue: delegateQueue) pinger.delegate = self } deinit { - stopNoQueue() + stop() } func start(probeAddress: IPv4Address) { - internalQueue.async { - self.startNoQueue(probeAddress: probeAddress) + nslock.lock() + defer { nslock.unlock() } + + if case .stopped = state.connectionState { + logger.debug("Start with address: \(probeAddress).") + } else { + _stop(forRestart: true) + logger.debug("Restart with address: \(probeAddress).") } + + self.probeAddress = probeAddress + state.connectionState = .pendingStart + + startPathMonitor() } func stop() { - internalQueue.async { - self.stopNoQueue() - } + nslock.lock() + defer { nslock.unlock() } + + _stop() } func onWake() { - internalQueue.async { - self.onWakeNoQueue() + nslock.lock() + defer { nslock.unlock() } + + logger.trace("Wake up.") + + switch state.connectionState { + case .connecting, .connected: + startConnectivityCheckTimer() + startPathMonitor() + + case .waitingConnectivity, .pendingStart: + startPathMonitor() + + case .stopped, .recovering: + break } } func onSleep(completion: @escaping () -> Void) { - internalQueue.async { - self.onSleepNoQueue() - completion() - } + nslock.lock() + defer { nslock.unlock() } + + logger.trace("Prepare to sleep.") + + stopConnectivityCheckTimer() + stopPathMonitor() } // MARK: - PingerDelegate @@ -300,26 +336,7 @@ final class TunnelMonitor: PingerDelegate { // MARK: - Private - private func startNoQueue(probeAddress: IPv4Address) { - if case .stopped = state.connectionState { - logger.debug("Start with address: \(probeAddress).") - } else { - stopNoQueue(forRestart: true) - logger.debug("Restart with address: \(probeAddress).") - } - - self.probeAddress = probeAddress - state.connectionState = .pendingStart - - let pathMonitor = NWPathMonitor() - pathMonitor.pathUpdateHandler = { [weak self] path in - self?.handleNetworkPathUpdate(path) - } - pathMonitor.start(queue: internalQueue) - self.pathMonitor = pathMonitor - } - - private func stopNoQueue(forRestart: Bool = false) { + private func _stop(forRestart: Bool = false) { if case .stopped = state.connectionState { return } @@ -330,15 +347,38 @@ final class TunnelMonitor: PingerDelegate { probeAddress = nil - pathMonitor?.cancel() - pathMonitor = nil - + stopPathMonitor() stopMonitoring(resetRetryAttempt: !forRestart) state.connectionState = .stopped } + private func startPathMonitor() { + let pathMonitor = NWPathMonitor() + pathMonitor.pathUpdateHandler = { [weak self] path in + self?.handleNetworkPathUpdate(path) + } + pathMonitor.start(queue: eventQueue) + + self.pathMonitor?.cancel() + self.pathMonitor = pathMonitor + + logger.trace("Start path monitor.") + } + + private func stopPathMonitor() { + guard let pathMonitor = pathMonitor else { return } + + logger.trace("Stop path monitor.") + + pathMonitor.cancel() + self.pathMonitor = nil + } + private func checkConnectivity() { + nslock.lock() + defer { nslock.unlock() } + guard let probeAddress = probeAddress, let newStats = getStats() else { return } @@ -404,25 +444,14 @@ final class TunnelMonitor: PingerDelegate { #endif private func startConnectionRecovery() { - stopConnectivityCheckTimer() + stopPathMonitor() + stopMonitoring(resetRetryAttempt: false) - state.pingStats = PingStats() - state.isHeartbeatSuspended = false state.retryAttempt = state.retryAttempt.saturatingAddition(1) + state.connectionState = .recovering + probeAddress = nil - sendDelegateShouldHandleConnectionRecovery { [weak self] in - guard let self = self else { return } - - self.internalQueue.async { - switch self.state.connectionState { - case .connecting, .connected: - self.startConnectivityCheckTimer() - - case .pendingStart, .stopped, .waitingConnectivity: - break - } - } - } + sendDelegateShouldHandleConnectionRecovery() } private func sendPing(to receiver: IPv4Address, now: Date) { @@ -437,6 +466,9 @@ final class TunnelMonitor: PingerDelegate { } private func handleNetworkPathUpdate(_ networkPath: Network.NWPath) { + nslock.lock() + defer { nslock.unlock() } + let pathStatus = networkPath.status let isReachable = pathStatus == .requiresConnection || pathStatus == .satisfied let hasPhysicalNetworkInterface = networkPath.availableInterfaces.contains { nw in @@ -476,12 +508,15 @@ final class TunnelMonitor: PingerDelegate { stopMonitoring(resetRetryAttempt: true) sendDelegateNetworkStatusChange(false) - default: + case .stopped, .recovering: break } } private func didReceivePing(from sender: IPAddress, icmpHeader: ICMPHeader) { + nslock.lock() + defer { nslock.unlock() } + guard let probeAddress = probeAddress else { return } if sender.rawValue != probeAddress.rawValue { @@ -545,7 +580,7 @@ final class TunnelMonitor: PingerDelegate { } private func startConnectivityCheckTimer() { - let timer = DispatchSource.makeTimerSource(queue: internalQueue) + let timer = DispatchSource.makeTimerSource(queue: eventQueue) timer.setEventHandler { [weak self] in self?.checkConnectivity() } @@ -556,35 +591,17 @@ final class TunnelMonitor: PingerDelegate { self.timer = timer state.timeoutReference = Date() - } - private func stopConnectivityCheckTimer() { - timer?.cancel() - timer = nil + logger.trace("Start connectivity check timer.") } - private func onWakeNoQueue() { - logger.trace("Wake up.") - - switch state.connectionState { - case .connecting, .connected: - startConnectivityCheckTimer() - - case .pendingStart, .stopped, .waitingConnectivity: - break - } - } + private func stopConnectivityCheckTimer() { + guard let timer = timer else { return } - private func onSleepNoQueue() { - logger.trace("Prepare to sleep.") + logger.trace("Stop connectivity check timer.") - switch state.connectionState { - case .connecting, .connected: - stopConnectivityCheckTimer() - - case .pendingStart, .stopped, .waitingConnectivity: - break - } + timer.cancel() + self.timer = nil } private func sendDelegateConnectionEstablished() { @@ -593,12 +610,9 @@ final class TunnelMonitor: PingerDelegate { } } - private func sendDelegateShouldHandleConnectionRecovery(completion: @escaping () -> Void) { + private func sendDelegateShouldHandleConnectionRecovery() { delegateQueue.async { - self.delegate?.tunnelMonitorDelegate( - self, - shouldHandleConnectionRecoveryWithCompletion: completion - ) + self.delegate?.tunnelMonitorDelegateShouldHandleConnectionRecovery(self) } } diff --git a/ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift b/ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift index 51c47419c1..9839faf3e5 100644 --- a/ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift +++ b/ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift @@ -13,10 +13,7 @@ protocol TunnelMonitorDelegate: AnyObject { func tunnelMonitorDidDetermineConnectionEstablished(_ tunnelMonitor: TunnelMonitor) /// Invoked when tunnel monitor determined that connection attempt has failed. - func tunnelMonitorDelegate( - _ tunnelMonitor: TunnelMonitor, - shouldHandleConnectionRecoveryWithCompletion completionHandler: @escaping () -> Void - ) + func tunnelMonitorDelegateShouldHandleConnectionRecovery(_ tunnelMonitor: TunnelMonitor) /// Invoked when network reachability status changes. func tunnelMonitor( |
