summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2025-01-31 15:14:41 +0100
committerBug Magnet <marco.nikic@mullvad.net>2025-02-11 09:31:31 +0100
commit0421eadffbe57f73b920225b397adf6cd0789f88 (patch)
tree1a08f49c915587237aea41418148feed8d5b6fe9
parenta5690d0381aea34bf5fde83fd206aee75a119f72 (diff)
downloadmullvadvpn-0421eadffbe57f73b920225b397adf6cd0789f88.tar.xz
mullvadvpn-0421eadffbe57f73b920225b397adf6cd0789f88.zip
Add TunnelMonitorActor
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/PacketTunnelCore/Pinger/PingerProtocol.swift4
-rw-r--r--ios/PacketTunnelCore/TunnelMonitor/TunnelDeviceInfoProtocol.swift2
-rw-r--r--ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift5
-rw-r--r--ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorActor.swift218
-rw-r--r--ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift24
-rw-r--r--ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorState.swift12
-rw-r--r--ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorTimings.swift2
-rw-r--r--ios/PacketTunnelCore/TunnelMonitor/WgStats.swift2
9 files changed, 265 insertions, 8 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 7a4a5271e0..d7c2c202a1 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -753,6 +753,7 @@
A939661B2CAE6CE1008128CA /* MigrationManagerMultiProcessUpgradeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A939661A2CAE6CE1008128CA /* MigrationManagerMultiProcessUpgradeTests.swift */; };
A93969812CE606190032A7A0 /* Maybenot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9840BB32C69F78A0030F05E /* Maybenot.swift */; };
A93A0BEC2D4CDE97001C9246 /* TunnelSettingsV7.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93A0BEB2D4CDE97001C9246 /* TunnelSettingsV7.swift */; };
+ A93A0BEE2D4CEA7B001C9246 /* TunnelMonitorActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93A0BED2D4CEA7B001C9246 /* TunnelMonitorActor.swift */; };
A94D691A2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E22AA72AE9003D1918 /* WireGuardKitTypes */; };
A94D691B2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E72AA7399D003D1918 /* WireGuardKitTypes */; };
A95EEE362B722CD600A8A39B /* TunnelMonitorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95EEE352B722CD600A8A39B /* TunnelMonitorState.swift */; };
@@ -2162,6 +2163,7 @@
A935594D2B4E919F00D5D524 /* Api.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Api.xcconfig; sourceTree = "<group>"; };
A939661A2CAE6CE1008128CA /* MigrationManagerMultiProcessUpgradeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationManagerMultiProcessUpgradeTests.swift; sourceTree = "<group>"; };
A93A0BEB2D4CDE97001C9246 /* TunnelSettingsV7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV7.swift; sourceTree = "<group>"; };
+ A93A0BED2D4CEA7B001C9246 /* TunnelMonitorActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorActor.swift; sourceTree = "<group>"; };
A944F2712B8E02E800473F4C /* libmullvad_ios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmullvad_ios.a; path = "../target/aarch64-apple-ios/debug/libmullvad_ios.a"; sourceTree = "<group>"; };
A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTests.swift; sourceTree = "<group>"; };
A948809A2BC9308D0090A44C /* EphemeralPeerExchangeActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralPeerExchangeActor.swift; sourceTree = "<group>"; };
@@ -3883,6 +3885,7 @@
A95EEE352B722CD600A8A39B /* TunnelMonitorState.swift */,
7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */,
58A3BDAF28A1821A00C8C2C6 /* WgStats.swift */,
+ A93A0BED2D4CEA7B001C9246 /* TunnelMonitorActor.swift */,
);
path = TunnelMonitor;
sourceTree = "<group>";
@@ -5861,6 +5864,7 @@
44B3C43A2BFE2C800079782C /* PacketTunnelActorReducer.swift in Sources */,
7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */,
A95EEE362B722CD600A8A39B /* TunnelMonitorState.swift in Sources */,
+ A93A0BEE2D4CEA7B001C9246 /* TunnelMonitorActor.swift in Sources */,
58FE25DB2AA72A8F003D1918 /* StartOptions.swift in Sources */,
A97D25AE2B0BB18100946B2D /* ProtocolObfuscator.swift in Sources */,
583832212AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift in Sources */,
diff --git a/ios/PacketTunnelCore/Pinger/PingerProtocol.swift b/ios/PacketTunnelCore/Pinger/PingerProtocol.swift
index 1c06c634f9..af9bc31e69 100644
--- a/ios/PacketTunnelCore/Pinger/PingerProtocol.swift
+++ b/ios/PacketTunnelCore/Pinger/PingerProtocol.swift
@@ -29,8 +29,8 @@ public struct PingerSendResult {
}
/// A type capable of sending and receving ICMP traffic.
-public protocol PingerProtocol {
- var onReply: ((PingerReply) -> Void)? { get set }
+public protocol PingerProtocol: AnyObject, Sendable {
+ var onReply: (@Sendable (PingerReply) -> Void)? { get set }
func startPinging(destAddress: IPv4Address) throws
func stopPinging()
diff --git a/ios/PacketTunnelCore/TunnelMonitor/TunnelDeviceInfoProtocol.swift b/ios/PacketTunnelCore/TunnelMonitor/TunnelDeviceInfoProtocol.swift
index 664fd69ed9..d9a0a33250 100644
--- a/ios/PacketTunnelCore/TunnelMonitor/TunnelDeviceInfoProtocol.swift
+++ b/ios/PacketTunnelCore/TunnelMonitor/TunnelDeviceInfoProtocol.swift
@@ -9,7 +9,7 @@
import Foundation
/// A type that can provide statistics and basic information about tunnel device.
-public protocol TunnelDeviceInfoProtocol {
+public protocol TunnelDeviceInfoProtocol: Sendable {
/// Returns tunnel interface name (i.e utun0) if available.
var interfaceName: String? { get }
diff --git a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift
index 778b8ac417..c27c5b0a20 100644
--- a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift
+++ b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift
@@ -21,9 +21,8 @@ public final class TunnelMonitor: TunnelMonitorProtocol {
private let eventQueue: DispatchQueue
private let timings: TunnelMonitorTimings
- private var pinger: PingerProtocol
- private var isObservingDefaultPath = false
- private var timer: DispatchSourceTimer?
+ private let pinger: PingerProtocol
+ nonisolated(unsafe) private var timer: DispatchSourceTimer?
private var state: TunnelMonitorState
private var probeAddress: IPv4Address?
diff --git a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorActor.swift b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorActor.swift
new file mode 100644
index 0000000000..051f4d9c49
--- /dev/null
+++ b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorActor.swift
@@ -0,0 +1,218 @@
+//
+// TunnelMonitorActor.swift
+// PacketTunnelCore
+//
+// Created by Marco Nikic on 2025-01-31.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadLogging
+import MullvadTypes
+import Network
+import NetworkExtension
+
+public actor TunnelMonitorActor: TunnelMonitorActorProtocol {
+ private let pinger: any PingerProtocol
+ private let timings: TunnelMonitorTimings
+ private var state: TunnelMonitorState
+ private let tunnelDeviceInfo: any TunnelDeviceInfoProtocol
+ private var probeAddress: IPv4Address?
+ private var timer: DispatchSourceTimer?
+ private var eventHandler: AsyncStream<TunnelMonitorEvent>.Continuation?
+
+ public let eventStream: AsyncStream<TunnelMonitorEvent>
+
+ init(
+ pinger: any PingerProtocol,
+ timings: TunnelMonitorTimings,
+ tunnelDeviceInfo: any TunnelDeviceInfoProtocol
+ ) {
+ self.pinger = pinger
+ self.timings = timings
+ self.state = TunnelMonitorState(timings: timings)
+ self.tunnelDeviceInfo = tunnelDeviceInfo
+
+ var innerContinuation: AsyncStream<TunnelMonitorEvent>.Continuation?
+ let stream = AsyncStream<TunnelMonitorEvent> { continuation in
+ innerContinuation = continuation
+ }
+
+ self.eventStream = stream
+ self.eventHandler = innerContinuation
+ }
+
+ // MARK: - Public API
+
+ public func start(probeAddress: IPv4Address) async throws {
+ print(#function)
+ guard state.connectionState == .stopped else { return }
+
+ self.probeAddress = probeAddress
+ self.state.connectionState = .pendingStart
+ pinger.onReply = { [weak self] reply in
+ Task {
+ await self?.handlePingerReply(reply)
+ }
+ }
+
+ try startMonitoring()
+ }
+
+ public func stop() async {
+ print(#function)
+ _stop()
+ }
+
+ public func wake() async {
+ print(#function)
+ }
+
+ public func sleep() async {
+ print(#function)
+ }
+
+ public func handleNetworkPathUpdate(_ networkPath: any NetworkPath) async {
+ print(#function)
+
+ let pathStatus = networkPath.status
+ let isReachable = pathStatus == .satisfiable || pathStatus == .satisfied
+
+ switch state.connectionState {
+ case .pendingStart:
+ if isReachable {
+ try? startMonitoring()
+ } else {
+ state.connectionState = .waitingConnectivity
+ }
+
+ case .waitingConnectivity:
+ guard isReachable else { return }
+
+ try? startMonitoring()
+
+ case .connecting, .connected:
+ guard !isReachable else { return }
+
+ state.connectionState = .waitingConnectivity
+ stopMonitoring(resetRetryAttempt: true)
+
+ case .stopped, .recovering:
+ break
+ }
+ }
+
+ // MARK: - Private API
+
+ private func _stop(restarting: Bool = false) {
+ guard state.connectionState != .stopped else { return }
+
+ probeAddress = nil
+ stopMonitoring(resetRetryAttempt: restarting == false)
+ state.connectionState = .stopped
+ }
+
+ // TODO: Should this be marked `throws` ?
+ private func startMonitoring() throws {
+ guard let probeAddress else { return }
+
+ try pinger.startPinging(destAddress: probeAddress)
+ state.connectionState = .connecting
+ startConnectivityCheckTimer()
+ }
+
+ private func stopMonitoring(resetRetryAttempt: Bool) {
+ stopConnectivityCheckTimer()
+ pinger.stopPinging()
+ state.reset(resetRetryAttempts: resetRetryAttempt)
+ }
+
+ private func handlePingerReply(_ reply: PingerReply) {}
+
+ private func checkConnectivity() {
+ guard let newStats = try? tunnelDeviceInfo.getStats(),
+ state.connectionState == .connecting || state.connectionState == .connected
+ else { return }
+
+ // Check if counters were reset.
+ let isStatsReset = newStats.bytesReceived < state.netStats.bytesReceived ||
+ newStats.bytesSent < state.netStats.bytesSent
+
+ guard !isStatsReset else {
+ state.netStats = newStats
+ return
+ }
+
+ let now = Date()
+ state.updateNetStats(newStats: newStats, now: now)
+
+ let timeout = state.getPingTimeout()
+ let evaluation = state.evaluateConnection(now: now, pingTimeout: timeout)
+
+ switch evaluation {
+ case .ok:
+ break
+
+ case .pingTimeout:
+ startConnectionRecovery()
+
+ case .suspendHeartbeat:
+ state.isHeartbeatSuspended = true
+
+ case .sendHeartbeatPing, .retryHeartbeatPing, .sendNextPing, .sendInitialPing,
+ .inboundTrafficTimeout, .trafficTimeout:
+ if state.isHeartbeatSuspended {
+ state.isHeartbeatSuspended = false
+ state.timeoutReference = now
+ }
+ sendPing(now: now)
+ }
+ }
+
+ private func sendPing(now: Date) {
+ do {
+ let sendResult = try pinger.send()
+ state.updatePingStats(sendResult: sendResult, now: now)
+ } catch {
+ print("Failed to send ping.")
+ }
+ }
+
+ private func sendConnectionEstablishedEvent() {
+ eventHandler?.yield(.connectionEstablished)
+ }
+
+ private func sendConnectionLostEvent() {
+ eventHandler?.yield(.connectionLost)
+ }
+
+ private func startConnectionRecovery() {
+ stopMonitoring(resetRetryAttempt: false)
+
+ state.retryAttempt = state.retryAttempt.saturatingAddition(1)
+ state.connectionState = .recovering
+ probeAddress = nil
+
+ sendConnectionLostEvent()
+ }
+
+ private func startConnectivityCheckTimer() {
+ print(#function)
+
+ guard timer?.isCancelled == false else { return }
+
+ let timerSource = DispatchSource.makeTimerSource()
+ timerSource.setEventHandler(handler: checkConnectivity)
+ timerSource.schedule(wallDeadline: .now(), repeating: timings.connectivityCheckInterval.timeInterval)
+ timerSource.activate()
+
+ timer?.cancel()
+ timer = timerSource
+
+ state.timeoutReference = Date()
+ }
+
+ private func stopConnectivityCheckTimer() {
+ print(#function)
+ }
+}
diff --git a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift
index 5bc9d8e5f5..77bdeb7671 100644
--- a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift
+++ b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift
@@ -42,3 +42,27 @@ public protocol TunnelMonitorProtocol: AnyObject, Sendable {
/// Handle changes in network path, eg. update connection state and monitoring.
func handleNetworkPathUpdate(_ networkPath: NetworkPath)
}
+
+/// A type that can provide tunnel monitoring.
+public protocol TunnelMonitorActorProtocol: Sendable {
+ /// Event handler that starts receiving events after the call to `start(probeAddress:)`.
+ var eventStream: AsyncStream<TunnelMonitorEvent> { get }
+
+ /// Start monitoring connection by pinging the given IP address.
+ /// Normally we should only give an address of a tunnel gateway here which is reachable over tunnel interface.
+ func start(probeAddress: IPv4Address) async throws
+
+ /// Stop monitoring connection.
+ func stop() async
+
+ /// Restarts internal timers and gracefully handles transition from sleep to awake device state.
+ /// Call this method when packet tunnel provider receives a wake event.
+ func wake() async
+
+ /// Cancels internal timers and time dependent data in preparation for device sleep.
+ /// Call this method when packet tunnel provider receives a sleep event.
+ func sleep() async
+
+ /// Handle changes in network path, eg. update connection state and monitoring.
+ func handleNetworkPathUpdate(_ networkPath: NetworkPath) async
+}
diff --git a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorState.swift b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorState.swift
index 6efdb8e476..47b57467d1 100644
--- a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorState.swift
+++ b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorState.swift
@@ -189,4 +189,16 @@ struct TunnelMonitorState {
return .ok
}
+
+ mutating func reset(resetRetryAttempts: Bool) {
+ netStats = WgStats()
+ lastSeenRx = nil
+ lastSeenTx = nil
+ pingStats = PingStats()
+ isHeartbeatSuspended = false
+
+ if resetRetryAttempts {
+ retryAttempt = 0
+ }
+ }
}
diff --git a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorTimings.swift b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorTimings.swift
index 7e925ec932..ee1836bb22 100644
--- a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorTimings.swift
+++ b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorTimings.swift
@@ -8,7 +8,7 @@
import MullvadTypes
-public struct TunnelMonitorTimings {
+public struct TunnelMonitorTimings: Sendable {
/// Interval for periodic heartbeat ping issued when traffic is flowing.
/// Should help to detect connectivity issues on networks that drop traffic in one of directions,
/// regardless if tx/rx counters are being updated.
diff --git a/ios/PacketTunnelCore/TunnelMonitor/WgStats.swift b/ios/PacketTunnelCore/TunnelMonitor/WgStats.swift
index a4fb0fe2a3..b227efa25a 100644
--- a/ios/PacketTunnelCore/TunnelMonitor/WgStats.swift
+++ b/ios/PacketTunnelCore/TunnelMonitor/WgStats.swift
@@ -8,7 +8,7 @@
import Foundation
-public struct WgStats {
+public struct WgStats: Sendable {
public let bytesReceived: UInt64
public let bytesSent: UInt64