Skip to content

Commit 3a2e118

Browse files
committed
Improve PacketTunnelPathObserver
1 parent 28a43fb commit 3a2e118

17 files changed

+69
-144
lines changed

ios/MullvadVPN/TunnelManager/TunnelManager.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ final class TunnelManager: StorePaymentObserver, @unchecked Sendable {
876876
private func startNetworkMonitor() {
877877
cancelNetworkMonitor()
878878

879-
networkMonitor = NWPathMonitor()
879+
networkMonitor = NWPathMonitor(prohibitedInterfaceTypes: [.other])
880880
networkMonitor?.pathUpdateHandler = { [weak self] path in
881881
self?.didUpdateNetworkPath(path)
882882
}
@@ -885,6 +885,7 @@ final class TunnelManager: StorePaymentObserver, @unchecked Sendable {
885885
}
886886

887887
private func cancelNetworkMonitor() {
888+
networkMonitor?.pathUpdateHandler = nil
888889
networkMonitor?.cancel()
889890
networkMonitor = nil
890891
}

ios/PacketTunnel/PacketTunnelProvider/PacketTunnelPathObserver.swift

+29-27
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,50 @@
77
//
88

99
import Combine
10+
import MullvadLogging
11+
import MullvadTypes
12+
import Network
1013
import NetworkExtension
1114
import PacketTunnelCore
1215

13-
final class PacketTunnelPathObserver: DefaultPathObserverProtocol, @unchecked Sendable {
14-
private weak var packetTunnelProvider: NEPacketTunnelProvider?
15-
private let stateLock = NSLock()
16-
private var pathUpdatePublisher: AnyCancellable?
16+
final class PacketTunnelPathObserver: DefaultPathObserverProtocol, Sendable {
1717
private let eventQueue: DispatchQueue
18+
private let pathMonitor: NWPathMonitor
19+
nonisolated(unsafe) let logger = Logger(label: "PacketTunnelPathObserver")
20+
private let stateLock = NSLock()
1821

19-
init(packetTunnelProvider: NEPacketTunnelProvider, eventQueue: DispatchQueue) {
20-
self.packetTunnelProvider = packetTunnelProvider
21-
self.eventQueue = eventQueue
22+
nonisolated(unsafe) private var started = false
23+
24+
public var currentPathStatus: Network.NWPath.Status {
25+
stateLock.withLock {
26+
pathMonitor.currentPath.status
27+
}
2228
}
2329

24-
var defaultPath: NetworkPath? {
25-
return packetTunnelProvider?.defaultPath
30+
init(eventQueue: DispatchQueue) {
31+
self.eventQueue = eventQueue
32+
33+
pathMonitor = NWPathMonitor(prohibitedInterfaceTypes: [.other])
2634
}
2735

28-
func start(_ body: @escaping @Sendable (NetworkPath) -> Void) {
36+
func start(_ body: @escaping @Sendable (Network.NWPath.Status) -> Void) {
2937
stateLock.withLock {
30-
pathUpdatePublisher?.cancel()
31-
32-
// Normally packet tunnel provider should exist throughout the network extension lifetime.
33-
pathUpdatePublisher = packetTunnelProvider?.publisher(for: \.defaultPath)
34-
.removeDuplicates(by: { oldPath, newPath in
35-
oldPath?.status == newPath?.status
36-
})
37-
.throttle(for: .seconds(2), scheduler: eventQueue, latest: true)
38-
.sink { change in
39-
if let change {
40-
body(change)
41-
}
42-
}
38+
guard started == false else { return }
39+
defer { started = true }
40+
pathMonitor.pathUpdateHandler = { updatedPath in
41+
body(updatedPath.status)
42+
}
43+
44+
pathMonitor.start(queue: eventQueue)
4345
}
4446
}
4547

4648
func stop() {
4749
stateLock.withLock {
48-
pathUpdatePublisher?.cancel()
49-
pathUpdatePublisher = nil
50+
guard started == true else { return }
51+
defer { started = false }
52+
pathMonitor.pathUpdateHandler = nil
53+
pathMonitor.cancel()
5054
}
5155
}
5256
}
53-
54-
extension NetworkExtension.NWPath: NetworkPath {}

ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
9191
timings: PacketTunnelActorTimings(),
9292
tunnelAdapter: adapter,
9393
tunnelMonitor: tunnelMonitor,
94-
defaultPathObserver: PacketTunnelPathObserver(packetTunnelProvider: self, eventQueue: internalQueue),
94+
defaultPathObserver: PacketTunnelPathObserver(eventQueue: internalQueue),
9595
blockedStateErrorMapper: BlockedStateErrorMapper(),
9696
relaySelector: relaySelector,
9797
settingsReader: TunnelSettingsManager(settingsReader: SettingsReader()) { [weak self] settings in

ios/PacketTunnelCore/Actor/NetworkPath+NetworkReachability.swift

+9-11
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,20 @@
77
//
88

99
import Foundation
10+
import Network
1011

11-
extension NetworkPath {
12+
extension Network.NWPath.Status {
1213
/// Converts `NetworkPath.status` into `NetworkReachability`.
1314
var networkReachability: NetworkReachability {
14-
switch status {
15-
case .satisfiable, .satisfied:
16-
return .reachable
17-
15+
switch self {
16+
case .satisfied:
17+
.reachable
1818
case .unsatisfied:
19-
return .unreachable
20-
21-
case .invalid:
22-
return .undetermined
23-
19+
.unreachable
20+
case .requiresConnection:
21+
.reachable
2422
@unknown default:
25-
return .undetermined
23+
.undetermined
2624
}
2725
}
2826
}

ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift

-41
Original file line numberDiff line numberDiff line change
@@ -16,45 +16,4 @@ extension PacketTunnelActor {
1616
self?.eventChannel.send(.monitorEvent(event))
1717
}
1818
}
19-
20-
/**
21-
Handle tunnel monitor event.
22-
23-
Invoked by comand consumer.
24-
25-
- Important: this method will suspend and must only be invoked as a part of channel consumer to guarantee transactional execution.
26-
*/
27-
func handleMonitorEvent(_ event: TunnelMonitorEvent) async {
28-
switch event {
29-
case .connectionEstablished:
30-
onEstablishConnection()
31-
32-
case .connectionLost:
33-
await onHandleConnectionRecovery()
34-
}
35-
}
36-
37-
/// Reset connection attempt counter and update actor state to `connected` state once connection is established.
38-
private func onEstablishConnection() {
39-
switch state {
40-
case var .connecting(connState), var .reconnecting(connState):
41-
// Reset connection attempt once successfully connected.
42-
connState.connectionAttemptCount = 0
43-
state = .connected(connState)
44-
45-
case .initial, .connected, .disconnecting, .disconnected, .error, .negotiatingEphemeralPeer:
46-
break
47-
}
48-
}
49-
50-
/// Tell the tunnel to reconnect providing the correct reason to ensure that the attempt counter is incremented before reconnect.
51-
private func onHandleConnectionRecovery() async {
52-
switch state {
53-
case .connecting, .reconnecting, .connected:
54-
eventChannel.send(.reconnect(.random, reason: .connectionLoss))
55-
56-
case .initial, .disconnected, .disconnecting, .error, .negotiatingEphemeralPeer:
57-
break
58-
}
59-
}
6019
}

ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ extension PacketTunnelActor {
6262
relayConstraints: nil,
6363
currentKey: nil,
6464
keyPolicy: .useCurrent,
65-
networkReachability: defaultPathObserver.defaultPath?.networkReachability ?? .undetermined,
65+
networkReachability: defaultPathObserver.currentPathStatus.networkReachability,
6666
recoveryTask: startRecoveryTaskIfNeeded(reason: reason),
6767
priorState: .initial
6868
)

ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift

+3-6
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,20 @@
77
//
88

99
import Foundation
10+
import Network
1011

1112
extension PacketTunnelActor {
1213
/**
1314
Start observing changes to default path.
1415

1516
- Parameter notifyObserverWithCurrentPath: immediately notifies path observer with the current path when set to `true`.
1617
*/
17-
func startDefaultPathObserver(notifyObserverWithCurrentPath: Bool = false) {
18+
func startDefaultPathObserver() {
1819
logger.trace("Start default path observer.")
1920

2021
defaultPathObserver.start { [weak self] networkPath in
2122
self?.eventChannel.send(.networkReachability(networkPath))
2223
}
23-
24-
if notifyObserverWithCurrentPath, let currentPath = defaultPathObserver.defaultPath {
25-
eventChannel.send(.networkReachability(currentPath))
26-
}
2724
}
2825

2926
/// Stop observing changes to default path.
@@ -38,7 +35,7 @@ extension PacketTunnelActor {
3835

3936
- Parameter networkPath: new default path
4037
*/
41-
func handleDefaultPathChange(_ networkPath: NetworkPath) {
38+
func handleDefaultPathChange(_ networkPath: Network.NWPath.Status) {
4239
tunnelMonitor.handleNetworkPathUpdate(networkPath)
4340

4441
let newReachability = networkPath.networkReachability

ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift

+1-3
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,13 @@ extension PacketTunnelActor {
3636
return
3737
}
3838

39-
stopDefaultPathObserver()
40-
4139
state = .connecting(connectionData)
4240

4341
// Resume tunnel monitoring and use IPv4 gateway as a probe address.
4442
tunnelMonitor.start(probeAddress: connectionData.selectedRelays.exit.endpoint.ipv4Gateway)
4543
// Restart default path observer and notify the observer with the current path that might have changed while
4644
// path observer was paused.
47-
startDefaultPathObserver(notifyObserverWithCurrentPath: false)
45+
startDefaultPathObserver()
4846
}
4947

5048
/**

ios/PacketTunnelCore/Actor/PacketTunnelActor.swift

+2-9
Original file line numberDiff line numberDiff line change
@@ -294,17 +294,10 @@ extension PacketTunnelActor {
294294
connectionData: connectionState
295295
).make()
296296

297-
/*
298-
Stop default path observer while updating WireGuard configuration since it will call the system method
299-
`NEPacketTunnelProvider.setTunnelNetworkSettings()` which may cause active interfaces to go down making it look
300-
like network connectivity is not available, but only for a brief moment.
301-
*/
302-
stopDefaultPathObserver()
303-
304297
defer {
305298
// Restart default path observer and notify the observer with the current path that might have changed while
306299
// path observer was paused.
307-
startDefaultPathObserver(notifyObserverWithCurrentPath: true)
300+
startDefaultPathObserver()
308301
}
309302

310303
// Daita parameters are gotten from an ephemeral peer
@@ -342,7 +335,7 @@ extension PacketTunnelActor {
342335
reason: ActorReconnectReason
343336
) throws -> State.ConnectionData? {
344337
var keyPolicy: State.KeyPolicy = .useCurrent
345-
var networkReachability = defaultPathObserver.defaultPath?.networkReachability ?? .undetermined
338+
var networkReachability = defaultPathObserver.currentPathStatus.networkReachability
346339
var lastKeyRotation: Date?
347340

348341
let callRelaySelector = { [self] maybeCurrentRelays, connectionCount in

ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import Foundation
1010
import MullvadTypes
11+
import Network
1112
import WireGuardKitTypes
1213

1314
extension PacketTunnelActor {
@@ -35,7 +36,7 @@ extension PacketTunnelActor {
3536
case monitorEvent(_ event: TunnelMonitorEvent)
3637

3738
/// Network reachability events.
38-
case networkReachability(NetworkPath)
39+
case networkReachability(Network.NWPath.Status)
3940

4041
/// Update the device private key, as per post-quantum protocols
4142
case ephemeralPeerNegotiationStateChanged(EphemeralPeerNegotiationState, OneshotChannel)

ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import Foundation
1010
import MullvadTypes
11+
import Network
1112
import WireGuardKitTypes
1213

1314
extension PacketTunnelActor {
@@ -17,7 +18,7 @@ extension PacketTunnelActor {
1718
case stopDefaultPathObserver
1819
case startTunnelMonitor
1920
case stopTunnelMonitor
20-
case updateTunnelMonitorPath(NetworkPath)
21+
case updateTunnelMonitorPath(Network.NWPath.Status)
2122
case startConnection(NextRelays)
2223
case restartConnection(NextRelays, ActorReconnectReason)
2324

@@ -39,7 +40,7 @@ extension PacketTunnelActor {
3940
case (.stopDefaultPathObserver, .stopDefaultPathObserver): true
4041
case (.startTunnelMonitor, .startTunnelMonitor): true
4142
case (.stopTunnelMonitor, .stopTunnelMonitor): true
42-
case let (.updateTunnelMonitorPath(lp), .updateTunnelMonitorPath(rp)): lp.status == rp.status
43+
case let (.updateTunnelMonitorPath(lp), .updateTunnelMonitorPath(rp)): lp == rp
4344
case let (.startConnection(nr0), .startConnection(nr1)): nr0 == nr1
4445
case let (.restartConnection(nr0, rr0), .restartConnection(nr1, rr1)): nr0 == nr1 && rr0 == rr1
4546
case let (.reconnect(nr0), .reconnect(nr1)): nr0 == nr1

ios/PacketTunnelCore/TunnelMonitor/DefaultPathObserverProtocol.swift

+3-8
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,17 @@
77
//
88

99
import Foundation
10-
import NetworkExtension
10+
import Network
1111

1212
/// A type providing default path access and observation.
1313
public protocol DefaultPathObserverProtocol: Sendable {
1414
/// Returns current default path or `nil` if unknown yet.
15-
var defaultPath: NetworkPath? { get }
15+
var currentPathStatus: Network.NWPath.Status { get }
1616

1717
/// Start observing changes to `defaultPath`.
1818
/// This call must be idempotent. Multiple calls to start should replace the existing handler block.
19-
func start(_ body: @escaping @Sendable (NetworkPath) -> Void)
19+
func start(_ body: @escaping @Sendable (Network.NWPath.Status) -> Void)
2020

2121
/// Stop observing changes to `defaultPath`.
2222
func stop()
2323
}
24-
25-
/// A type that represents a network path.
26-
public protocol NetworkPath: Sendable {
27-
var status: NetworkExtension.NWPathStatus { get }
28-
}

ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,9 @@ public final class TunnelMonitor: TunnelMonitorProtocol {
122122
stopConnectivityCheckTimer()
123123
}
124124

125-
public func handleNetworkPathUpdate(_ networkPath: NetworkPath) {
125+
public func handleNetworkPathUpdate(_ networkPath: Network.NWPath.Status) {
126126
nslock.withLock {
127-
let pathStatus = networkPath.status
128-
let isReachable = pathStatus == .satisfiable || pathStatus == .satisfied
127+
let isReachable = networkPath == .satisfied || networkPath == .requiresConnection
129128

130129
switch state.connectionState {
131130
case .pendingStart:

ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,5 @@ public protocol TunnelMonitorProtocol: AnyObject, Sendable {
4040
func onSleep()
4141

4242
/// Handle changes in network path, eg. update connection state and monitoring.
43-
func handleNetworkPathUpdate(_ networkPath: NetworkPath)
43+
func handleNetworkPathUpdate(_ networkPath: Network.NWPath.Status)
4444
}

0 commit comments

Comments
 (0)