summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-10-08 13:15:11 +0200
committerJon Petersson <jon.petersson@mullvad.net>2025-10-14 09:54:28 +0200
commit8e2b42aa5a242fe8e150f646316ef94cfd78671c (patch)
tree58455be8fe764e521eb8f147f99477ac01ebdb49
parent0cc99647448b899b9f39c31e36620f4d42b464c4 (diff)
downloadmullvadvpn-8e2b42aa5a242fe8e150f646316ef94cfd78671c.tar.xz
mullvadvpn-8e2b42aa5a242fe8e150f646316ef94cfd78671c.zip
Move nw path monitoring outside packet tunnel actor
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift6
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift43
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift45
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift3
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift8
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor.swift23
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift7
-rw-r--r--ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift26
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")