diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-10-08 13:15:11 +0200 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-10-14 09:54:28 +0200 |
| commit | 8e2b42aa5a242fe8e150f646316ef94cfd78671c (patch) | |
| tree | 58455be8fe764e521eb8f147f99477ac01ebdb49 | |
| parent | 0cc99647448b899b9f39c31e36620f4d42b464c4 (diff) | |
| download | mullvadvpn-8e2b42aa5a242fe8e150f646316ef94cfd78671c.tar.xz mullvadvpn-8e2b42aa5a242fe8e150f646316ef94cfd78671c.zip | |
Move nw path monitoring outside packet tunnel actor
9 files changed, 49 insertions, 116 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index a3653dbe61..553bf4265d 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -146,7 +146,6 @@ 5838321B2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838321A2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift */; }; 5838321D2AC1C54600EA2071 /* TaskSleepTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */; }; 5838321F2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838321E2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift */; }; - 583832212AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832202AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift */; }; 583832232AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832222AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift */; }; 583832252AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832242AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift */; }; 583832272AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832262AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift */; }; @@ -1763,7 +1762,6 @@ 5838321A2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+Mocks.swift"; sourceTree = "<group>"; }; 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskSleepTests.swift; sourceTree = "<group>"; }; 5838321E2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+KeyPolicy.swift"; sourceTree = "<group>"; }; - 583832202AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+NetworkReachability.swift"; sourceTree = "<group>"; }; 583832222AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+ErrorState.swift"; sourceTree = "<group>"; }; 583832242AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+ConnectionMonitoring.swift"; sourceTree = "<group>"; }; 583832262AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+SleepCycle.swift"; sourceTree = "<group>"; }; @@ -3612,7 +3610,6 @@ 583832222AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift */, 58FE25F32AA9D730003D1918 /* PacketTunnelActor+Extensions.swift */, 5838321E2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift */, - 583832202AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift */, 44DF8AC32BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift */, 586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */, 583832262AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift */, @@ -6311,7 +6308,6 @@ A95EEE362B722CD600A8A39B /* TunnelMonitorState.swift in Sources */, 58FE25DB2AA72A8F003D1918 /* StartOptions.swift in Sources */, A97D25AE2B0BB18100946B2D /* ProtocolObfuscator.swift in Sources */, - 583832212AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift in Sources */, 58FE25D82AA72A8F003D1918 /* ConfigurationBuilder.swift in Sources */, 7AEF7F1A2AD00F52006FE45D /* AppMessageHandler.swift in Sources */, 580D6B8A2AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift in Sources */, diff --git a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift index 6117f2ec75..df1dc4431e 100644 --- a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift @@ -53,7 +53,6 @@ final class PacketTunnelActorReducerTests: XCTestCase { XCTAssertEqual( effects, [ - .startDefaultPathObserver, .startTunnelMonitor, .startConnection(.random), ]) @@ -71,7 +70,6 @@ final class PacketTunnelActorReducerTests: XCTestCase { XCTAssertEqual( effects, [ - .startDefaultPathObserver, .startTunnelMonitor, .startConnection(.preSelected(selectedRelays)), ]) @@ -91,7 +89,6 @@ final class PacketTunnelActorReducerTests: XCTestCase { effects, [ .stopTunnelMonitor, - .stopDefaultPathObserver, .stopTunnelAdapter, .setDisconnectedState, ]) @@ -109,7 +106,6 @@ final class PacketTunnelActorReducerTests: XCTestCase { effects, [ .stopTunnelMonitor, - .stopDefaultPathObserver, .stopTunnelAdapter, .setDisconnectedState, ]) @@ -127,7 +123,6 @@ final class PacketTunnelActorReducerTests: XCTestCase { effects, [ .stopTunnelMonitor, - .stopDefaultPathObserver, .stopTunnelAdapter, .setDisconnectedState, ]) @@ -150,7 +145,6 @@ final class PacketTunnelActorReducerTests: XCTestCase { XCTAssertEqual( effects, [ - .stopDefaultPathObserver, .stopTunnelAdapter, .setDisconnectedState, ]) diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index 9852e4a4e4..7a26115807 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -27,10 +27,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { private var adapter: WgAdapter! private var relaySelector: RelaySelectorWrapper! private var ephemeralPeerExchangingPipeline: EphemeralPeerExchangingPipeline! - private let tunnelSettingsUpdater: SettingsUpdater! - private let pathObserver: PacketTunnelPathObserver! + private let tunnelSettingsUpdater: SettingsUpdater + private let defaultPathObserver: PacketTunnelPathObserver private var encryptedDNSTransport: EncryptedDNSTransport! - private var migrationManager: MigrationManager! + private var migrationManager: MigrationManager let migrationFailureIterator = REST.RetryStrategy.failedMigrationRecovery.makeDelayIterator() private let tunnelSettingsListener = TunnelSettingsListener() @@ -58,7 +58,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { tunnelSettingsUpdater = SettingsUpdater(listener: tunnelSettingsListener) migrationManager = MigrationManager(cacheDirectory: containerURL) - pathObserver = PacketTunnelPathObserver(eventQueue: internalQueue) + defaultPathObserver = PacketTunnelPathObserver(eventQueue: internalQueue) super.init() @@ -105,7 +105,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { timings: PacketTunnelActorTimings(), tunnelAdapter: adapter, tunnelMonitor: tunnelMonitor, - defaultPathObserver: pathObserver, + defaultPathObserver: defaultPathObserver, blockedStateErrorMapper: BlockedStateErrorMapper(), relaySelector: relaySelector, settingsReader: TunnelSettingsManager(settingsReader: SettingsReader()) { [weak self] settings in @@ -115,6 +115,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { protocolObfuscator: ProtocolObfuscator<TunnelObfuscator>() ) + // Since PacketTunnelActor depends on the path observer, start observing after actor has been initalized. + startDefaultPathObserver() + let urlRequestProxy = URLRequestProxy( dispatchQueue: internalQueue, transportProvider: transportProvider @@ -186,11 +189,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { override func stopTunnel(with reason: NEProviderStopReason) async { providerLogger.debug("stopTunnel: \(ProviderStopReasonWrapper(reason: reason))") - stopObservingActorState() - actor.stop() - await actor.waitUntilDisconnected() + + stopObservingActorState() } override func handleAppMessage(_ messageData: Data) async -> Data? { @@ -328,6 +330,25 @@ extension PacketTunnelProvider { } } +// MARK: - Network path monitor observing + +extension PacketTunnelProvider { + + private func startDefaultPathObserver() { + providerLogger.trace("Start default path observer.") + + defaultPathObserver.start { [weak self] networkPath in + self?.actor.updateNetworkReachability(networkPathStatus: networkPath) + } + } + + private func stopDefaultPathObserver() { + providerLogger.trace("Stop default path observer.") + + defaultPathObserver.stop() + } +} + // MARK: - State observer extension PacketTunnelProvider { @@ -370,7 +391,9 @@ extension PacketTunnelProvider { observedConnectionState, privateKey: privateKey ) - case .initial, .connected, .disconnecting, .disconnected, .error: + case .disconnected: + stopDefaultPathObserver() + case .initial, .connected, .disconnecting, .error: break } } @@ -451,7 +474,7 @@ extension PacketTunnelProvider: EphemeralPeerReceiving { func ephemeralPeerExchangeFailed() { // Do not retry connection unless there's network reachability. Doing so will lead to a hot loop where // connections are retried every time peer exchange fails, which it will if reachability is not satisfied. - if pathObserver.currentPathStatus.networkReachability == .reachable { + if defaultPathObserver.currentPathStatus.networkReachability == .reachable { // Do not try reconnecting to the `.current` relay, else the actor's `State` equality check will fail // and it will not try to reconnect actor.reconnect(to: .random, reconnectReason: .connectionLoss) diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift deleted file mode 100644 index 975a80d3e2..0000000000 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// Actor+NetworkReachability.swift -// PacketTunnelCore -// -// Created by pronebird on 26/09/2023. -// Copyright © 2025 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import Network - -extension PacketTunnelActor { - /** - Start observing changes to default path. - - - Parameter notifyObserverWithCurrentPath: immediately notifies path observer with the current path when set to `true`. - */ - func startDefaultPathObserver() { - logger.trace("Start default path observer.") - - defaultPathObserver.start { [weak self] networkPath in - self?.eventChannel.send(.networkReachability(networkPath)) - } - } - - /// Stop observing changes to default path. - func stopDefaultPathObserver() { - logger.trace("Stop default path observer.") - - defaultPathObserver.stop() - } - - /** - Event handler that receives new network path from tunnel monitor and updates internal state with new network reachability status. - - - Parameter networkPath: new default path - */ - func handleDefaultPathChange(_ networkPath: Network.NWPath.Status) { - tunnelMonitor.handleNetworkPathUpdate(networkPath) - - let newReachability = networkPath.networkReachability - - state.mutateAssociatedData { $0.networkReachability = newReachability } - } -} diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift index 9733d10b8d..99c8442918 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift @@ -40,9 +40,6 @@ extension PacketTunnelActor { // Resume tunnel monitoring and use IPv4 gateway as a probe address. tunnelMonitor.start(probeAddress: connectionData.selectedRelays.exit.endpoint.ipv4Gateway) - // Restart default path observer and notify the observer with the current path that might have changed while - // path observer was paused. - startDefaultPathObserver() } /** diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift index ac4e784c63..626e3c47a9 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift @@ -8,6 +8,7 @@ import Foundation import MullvadTypes +import Network import WireGuardKitTypes /** @@ -35,6 +36,13 @@ extension PacketTunnelActor { } /** + Tell actor to update its network reachability. + */ + nonisolated public func updateNetworkReachability(networkPathStatus: NWPath.Status) { + eventChannel.send(.networkReachability(networkPathStatus)) + } + + /** Tell actor to reconnect the tunnel. - Parameter nextRelays: next relays to connect to. diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index d2561c1094..d912de581a 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -104,10 +104,6 @@ public actor PacketTunnelActor { func executeEffect(_ effect: Effect) async { switch effect { - case .startDefaultPathObserver: - startDefaultPathObserver() - case .stopDefaultPathObserver: - stopDefaultPathObserver() case .startTunnelMonitor: setTunnelMonitorEventHandler() case .stopTunnelMonitor: @@ -174,6 +170,14 @@ public actor PacketTunnelActor { } semaphore.send() } + + private func handleDefaultPathChange(_ networkPath: Network.NWPath.Status) { + tunnelMonitor.handleNetworkPathUpdate(networkPath) + + let newReachability = networkPath.networkReachability + + state.mutateAssociatedData { $0.networkReachability = newReachability } + } } // MARK: - @@ -191,9 +195,6 @@ extension PacketTunnelActor { logger.debug("\(options.logFormat())") - // Start observing default network path to determine network reachability. - startDefaultPathObserver() - // Assign a closure receiving tunnel monitor events. setTunnelMonitorEventHandler() @@ -218,8 +219,6 @@ extension PacketTunnelActor { fallthrough case .error: - stopDefaultPathObserver() - do { try await tunnelAdapter.stop() } catch { @@ -282,12 +281,6 @@ extension PacketTunnelActor { connectionData: connectionState ).make() - defer { - // Restart default path observer and notify the observer with the current path that might have changed while - // path observer was paused. - startDefaultPathObserver() - } - let entryConfiguration = configuration.entryConfiguration let exitConfiguration = configuration.exitConfiguration diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift index e81ec62a92..1a5e5b0e3a 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift @@ -14,8 +14,6 @@ import WireGuardKitTypes extension PacketTunnelActor { /// A structure encoding an effect; each event will yield zero or more of those, which can then be sequentially executed. enum Effect: Equatable, Sendable { - case startDefaultPathObserver - case stopDefaultPathObserver case startTunnelMonitor case stopTunnelMonitor case updateTunnelMonitorPath(Network.NWPath.Status) @@ -36,8 +34,6 @@ extension PacketTunnelActor { // We cannot synthesise Equatable on Effect because NetworkPath is a protocol which cannot be easily made Equatable, so we need to do this for now. static func == (lhs: PacketTunnelActor.Effect, rhs: PacketTunnelActor.Effect) -> Bool { return switch (lhs, rhs) { - case (.startDefaultPathObserver, .startDefaultPathObserver): true - case (.stopDefaultPathObserver, .stopDefaultPathObserver): true case (.startTunnelMonitor, .startTunnelMonitor): true case (.stopTunnelMonitor, .stopTunnelMonitor): true case let (.updateTunnelMonitorPath(lp), .updateTunnelMonitorPath(rp)): lp == rp @@ -61,7 +57,6 @@ extension PacketTunnelActor { case let .start(options): guard case .initial = state else { return [] } return [ - .startDefaultPathObserver, .startTunnelMonitor, .startConnection(options.selectedRelays.map { .preSelected($0) } ?? .random), ] @@ -109,13 +104,11 @@ extension PacketTunnelActor { state = .disconnecting(connState) return [ .stopTunnelMonitor, - .stopDefaultPathObserver, .stopTunnelAdapter, .setDisconnectedState, ] case .error: return [ - .stopDefaultPathObserver, .stopTunnelAdapter, .setDisconnectedState, ] diff --git a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift index 468859e787..0bacee4903 100644 --- a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift +++ b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift @@ -305,32 +305,6 @@ final class PacketTunnelActorTests: XCTestCase { await fulfillment(of: [disconnectedExpectation], timeout: .UnitTest.invertedTimeout) } - func testStopCancelsDefaultPathObserver() async throws { - let pathObserver = DefaultPathObserverFake() - let actor = PacketTunnelActor.mock(defaultPathObserver: pathObserver) - - let connectedStateExpectation = expectation(description: "Connected state") - let didStopObserverExpectation = expectation(description: "Did stop path observer") - pathObserver.onStop = { didStopObserverExpectation.fulfill() } - - let expression: (ObservedState) -> Bool = { if case .connected = $0 { true } else { false } } - - await expect(expression, on: actor) { - connectedStateExpectation.fulfill() - } - - actor.start(options: launchOptions) - await fulfillment(of: [connectedStateExpectation], timeout: .UnitTest.timeout) - - let disconnectedStateExpectation = expectation(description: "Disconnected state") - - await expect(.disconnected, on: actor) { - disconnectedStateExpectation.fulfill() - } - actor.stop() - await fulfillment(of: [disconnectedStateExpectation, didStopObserverExpectation], timeout: .UnitTest.timeout) - } - func testCannotEnterErrorStateWhenStopping() async throws { let actor = PacketTunnelActor.mock() let connectingStateExpectation = expectation(description: "Connecting state") |
