diff options
| author | Andrew Bulhak <andrew.bulhak@mullvad.net> | 2024-03-14 15:10:10 +0100 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2024-03-15 13:15:20 +0100 |
| commit | dcefe5829105efa137feb3128017dc0e3ce946f2 (patch) | |
| tree | efaadc2a1c51dcfd4919d0c97377f3adddb5359b | |
| parent | 7a74b8493ba7b2334a4bbed306d5ef32868bbab5 (diff) | |
| download | mullvadvpn-dcefe5829105efa137feb3128017dc0e3ce946f2.tar.xz mullvadvpn-dcefe5829105efa137feb3128017dc0e3ce946f2.zip | |
Move CommandChannel into the PacketTunnelActor namespace for consistency with Command
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 8 | ||||
| -rw-r--r-- | ios/PacketTunnelCore/Actor/Command.swift | 70 | ||||
| -rw-r--r-- | ios/PacketTunnelCore/Actor/CommandChannel.swift | 233 | ||||
| -rw-r--r-- | ios/PacketTunnelCore/Actor/PacketTunnelActor.swift | 3 | ||||
| -rw-r--r-- | ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift | 78 | ||||
| -rw-r--r-- | ios/PacketTunnelCoreTests/CommandChannelTests.swift | 10 |
6 files changed, 208 insertions, 194 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 7eba46f8dd..c7427d9ef1 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -106,7 +106,7 @@ 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 */; }; - 583832292AC3DF1300EA2071 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832282AC3DF1300EA2071 /* Command.swift */; }; + 583832292AC3DF1300EA2071 /* PacketTunnelActorCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832282AC3DF1300EA2071 /* PacketTunnelActorCommand.swift */; }; 5838322B2AC3EF9600EA2071 /* CommandChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838322A2AC3EF9600EA2071 /* CommandChannel.swift */; }; 583D86482A2678DC0060D63B /* DeviceStateAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */; }; 583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DA21325FA4B5C00318683 /* LocationDataSource.swift */; }; @@ -1407,7 +1407,7 @@ 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>"; }; - 583832282AC3DF1300EA2071 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; }; + 583832282AC3DF1300EA2071 /* PacketTunnelActorCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorCommand.swift; sourceTree = "<group>"; }; 5838322A2AC3EF9600EA2071 /* CommandChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandChannel.swift; sourceTree = "<group>"; }; 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStateAccessor.swift; sourceTree = "<group>"; }; 583DA21325FA4B5C00318683 /* LocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSource.swift; sourceTree = "<group>"; }; @@ -2752,7 +2752,7 @@ children = ( 58BDEBA02A9CA14B00F578F2 /* AnyTask.swift */, 58F3F3652AA086A400D3B0A4 /* AutoCancellingTask.swift */, - 583832282AC3DF1300EA2071 /* Command.swift */, + 583832282AC3DF1300EA2071 /* PacketTunnelActorCommand.swift */, 5838322A2AC3EF9600EA2071 /* CommandChannel.swift */, 583E60952A9F6D0800DC61EF /* ConfigurationBuilder.swift */, 580D6B892AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift */, @@ -4995,7 +4995,7 @@ 58C7A4552A863FB90060C66F /* TunnelMonitor.swift in Sources */, 58C7AF182ABD84AB007EDD7A /* ProxyURLResponse.swift in Sources */, 58C7A4512A863FB50060C66F /* PingerProtocol.swift in Sources */, - 583832292AC3DF1300EA2071 /* Command.swift in Sources */, + 583832292AC3DF1300EA2071 /* PacketTunnelActorCommand.swift in Sources */, 58CF95A22AD6F35800B59F5D /* ObservedState.swift in Sources */, 583832232AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift in Sources */, 58C7AF112ABD8480007EDD7A /* TunnelProviderMessage.swift in Sources */, diff --git a/ios/PacketTunnelCore/Actor/Command.swift b/ios/PacketTunnelCore/Actor/Command.swift deleted file mode 100644 index 668f444d49..0000000000 --- a/ios/PacketTunnelCore/Actor/Command.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// Command.swift -// PacketTunnelCore -// -// Created by pronebird on 27/09/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -/// Describes action that actor can perform. -enum Command { - /// Start tunnel. - case start(StartOptions) - - /// Stop tunnel. - case stop - - /// Reconnect tunnel. - case reconnect(NextRelay, reason: ReconnectReason = .userInitiated) - - /// Enter blocked state. - case error(BlockedStateReason) - - /// Notify that key rotation took place - case notifyKeyRotated(Date?) - - /// Switch to using the recently pushed WG key. - case switchKey - - /// Monitor events. - case monitorEvent(_ event: TunnelMonitorEvent) - - /// Network reachability events. - case networkReachability(NetworkPath) - - /// Format command for log output. - func logFormat() -> String { - switch self { - case .start: - return "start" - case .stop: - return "stop" - case let .reconnect(nextRelay, stopTunnelMonitor): - switch nextRelay { - case .current: - return "reconnect(current, \(stopTunnelMonitor))" - case .random: - return "reconnect(random, \(stopTunnelMonitor))" - case let .preSelected(selectedRelay): - return "reconnect(\(selectedRelay.hostname), \(stopTunnelMonitor))" - } - case let .error(reason): - return "error(\(reason))" - case .notifyKeyRotated: - return "notifyKeyRotated" - case let .monitorEvent(event): - switch event { - case .connectionEstablished: - return "monitorEvent(connectionEstablished)" - case .connectionLost: - return "monitorEvent(connectionLost)" - } - case .networkReachability: - return "networkReachability" - case .switchKey: - return "switchKey" - } - } -} diff --git a/ios/PacketTunnelCore/Actor/CommandChannel.swift b/ios/PacketTunnelCore/Actor/CommandChannel.swift index ca19794b96..e159100c34 100644 --- a/ios/PacketTunnelCore/Actor/CommandChannel.swift +++ b/ios/PacketTunnelCore/Actor/CommandChannel.swift @@ -47,167 +47,170 @@ import Foundation .reduce(into: [String]()) { $0.append($1) } ``` */ -final class CommandChannel: @unchecked Sendable { - private enum State { - /// Channel is active and running. - case active +extension PacketTunnelActor { + final class CommandChannel: @unchecked Sendable { + typealias Command = PacketTunnelActor.Command + private enum State { + /// Channel is active and running. + case active - /// Channel is awaiting for the buffer to be exhausted before ending all async iterations. - /// Publishing new values in this state is impossible. - case pendingEnd + /// Channel is awaiting for the buffer to be exhausted before ending all async iterations. + /// Publishing new values in this state is impossible. + case pendingEnd - /// Channel finished its work. - /// Publishing new values in this state is impossible. - /// An attempt to iterate over the channel in this state is equivalent to iterating over an empty array. - case finished - } + /// Channel finished its work. + /// Publishing new values in this state is impossible. + /// An attempt to iterate over the channel in this state is equivalent to iterating over an empty array. + case finished + } - /// A buffer of commands received but not consumed yet. - private var buffer: [Command] = [] + /// A buffer of commands received but not consumed yet. + private var buffer: [Command] = [] - /// Async continuations awaiting to receive the new value. - /// Continuations are stored here when there is no new value available for immediate delivery. - private var pendingContinuations: [CheckedContinuation<Command?, Never>] = [] + /// Async continuations awaiting to receive the new value. + /// Continuations are stored here when there is no new value available for immediate delivery. + private var pendingContinuations: [CheckedContinuation<Command?, Never>] = [] - private var state: State = .active - private var stateLock = NSLock() + private var state: State = .active + private var stateLock = NSLock() - init() {} + init() {} - deinit { - // Resume all continuations - finish() - } + deinit { + // Resume all continuations + finish() + } - /// Send command to consumer. - /// - /// - Parameter value: a new command. - func send(_ value: Command) { - stateLock.withLock { - guard case .active = state else { return } + /// Send command to consumer. + /// + /// - Parameter value: a new command. + func send(_ value: Command) { + stateLock.withLock { + guard case .active = state else { return } - buffer.append(value) + buffer.append(value) - if !pendingContinuations.isEmpty, let nextValue = consumeFirst() { - let continuation = pendingContinuations.removeFirst() - continuation.resume(returning: nextValue) + if !pendingContinuations.isEmpty, let nextValue = consumeFirst() { + let continuation = pendingContinuations.removeFirst() + continuation.resume(returning: nextValue) + } } } - } - /// Mark the end of channel but let consumers exchaust the buffer before declaring the end of iteration. - /// If the buffer is empty then it should resume all pending continuations and send them `nil` to mark the end of iteration. - func sendEnd() { - stateLock.withLock { - if case .active = state { - state = .pendingEnd + /// Mark the end of channel but let consumers exchaust the buffer before declaring the end of iteration. + /// If the buffer is empty then it should resume all pending continuations and send them `nil` to mark the end of iteration. + func sendEnd() { + stateLock.withLock { + if case .active = state { + state = .pendingEnd - if buffer.isEmpty { - state = .finished - sendEndToPendingContinuations() + if buffer.isEmpty { + state = .finished + sendEndToPendingContinuations() + } } } } - } - /// Flush buffered commands and resume all pending continuations sending them `nil` to mark the end of iteration. - func finish() { - stateLock.withLock { - switch state { - case .active, .pendingEnd: - state = .finished - buffer.removeAll() + /// Flush buffered commands and resume all pending continuations sending them `nil` to mark the end of iteration. + func finish() { + stateLock.withLock { + switch state { + case .active, .pendingEnd: + state = .finished + buffer.removeAll() - sendEndToPendingContinuations() + sendEndToPendingContinuations() - case .finished: - break + case .finished: + break + } } } - } - /// Send `nil` to mark the end of iteration to all pending continuations. - private func sendEndToPendingContinuations() { - for continuation in pendingContinuations { - continuation.resume(returning: nil) + /// Send `nil` to mark the end of iteration to all pending continuations. + private func sendEndToPendingContinuations() { + for continuation in pendingContinuations { + continuation.resume(returning: nil) + } + pendingContinuations.removeAll() } - pendingContinuations.removeAll() - } - /// Consume first message in the buffer. - /// Returns `nil` if the buffer is empty, otherwise if attempts to coalesce buffered commands before consuming the first comand in the list. - private func consumeFirst() -> Command? { - guard !buffer.isEmpty else { return nil } + /// Consume first message in the buffer. + /// Returns `nil` if the buffer is empty, otherwise if attempts to coalesce buffered commands before consuming the first comand in the list. + private func consumeFirst() -> Command? { + guard !buffer.isEmpty else { return nil } - coalesce() - return buffer.removeFirst() - } + coalesce() + return buffer.removeFirst() + } - /// Coalesce buffered commands to prevent future execution when the outcome is considered to be similar. - /// Mutates internal `buffer`. - private func coalesce() { - var i = buffer.count - 1 - while i > 0 { - defer { i -= 1 } + /// Coalesce buffered commands to prevent future execution when the outcome is considered to be similar. + /// Mutates internal `buffer`. + private func coalesce() { + var i = buffer.count - 1 + while i > 0 { + defer { i -= 1 } - assert(i < buffer.count) - let current = buffer[i] + assert(i < buffer.count) + let current = buffer[i] - // Remove all preceding commands when encountered "stop". - if case .stop = current { - buffer.removeFirst(i) - return - } + // Remove all preceding commands when encountered "stop". + if case .stop = current { + buffer.removeFirst(i) + return + } - // Coalesce earlier reconnection attempts into the most recent. - // This will rearrange the command buffer but hopefully should have no side effects. - if case .reconnect = current { - // Walk backwards starting with the preceding element. - for j in (0 ..< i).reversed() { - let preceding = buffer[j] - // Remove preceding reconnect and adjust the index of the outer loop. - if case .reconnect = preceding { - buffer.remove(at: j) - i -= 1 + // Coalesce earlier reconnection attempts into the most recent. + // This will rearrange the command buffer but hopefully should have no side effects. + if case .reconnect = current { + // Walk backwards starting with the preceding element. + for j in (0 ..< i).reversed() { + let preceding = buffer[j] + // Remove preceding reconnect and adjust the index of the outer loop. + if case .reconnect = preceding { + buffer.remove(at: j) + i -= 1 + } } } } } - } - private func next() async -> Command? { - return await withCheckedContinuation { continuation in - stateLock.withLock { - switch state { - case .pendingEnd: - if buffer.isEmpty { - state = .finished - continuation.resume(returning: nil) - } else { - // Keep consuming until the buffer is exhausted. - fallthrough - } + private func next() async -> Command? { + return await withCheckedContinuation { continuation in + stateLock.withLock { + switch state { + case .pendingEnd: + if buffer.isEmpty { + state = .finished + continuation.resume(returning: nil) + } else { + // Keep consuming until the buffer is exhausted. + fallthrough + } - case .active: - if let value = consumeFirst() { - continuation.resume(returning: value) - } else { - pendingContinuations.append(continuation) - } + case .active: + if let value = consumeFirst() { + continuation.resume(returning: value) + } else { + pendingContinuations.append(continuation) + } - case .finished: - continuation.resume(returning: nil) + case .finished: + continuation.resume(returning: nil) + } } } } } } -extension CommandChannel: AsyncSequence { +extension PacketTunnelActor.CommandChannel: AsyncSequence { typealias Element = Command struct AsyncIterator: AsyncIteratorProtocol { - let channel: CommandChannel + let channel: PacketTunnelActor.CommandChannel func next() async -> Command? { return await channel.next() } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index 4d40fdf1e6..e7a384d8b6 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -111,6 +111,9 @@ public actor PacketTunnelActor { case let .networkReachability(defaultPath): await handleDefaultPathChange(defaultPath) + + case .replaceDevicePrivateKey: + self.logger.warning("Not yet implemented") } } } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift new file mode 100644 index 0000000000..be325fc7b6 --- /dev/null +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift @@ -0,0 +1,78 @@ +// +// Command.swift +// PacketTunnelCore +// +// Created by pronebird on 27/09/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import WireGuardKitTypes + +extension PacketTunnelActor { + /// Describes action that actor can perform. + enum Command { + /// Start tunnel. + case start(StartOptions) + + /// Stop tunnel. + case stop + + /// Reconnect tunnel. + case reconnect(NextRelay, reason: ReconnectReason = .userInitiated) + + /// Enter blocked state. + case error(BlockedStateReason) + + /// Notify that key rotation took place + case notifyKeyRotated(Date?) + + /// Switch to using the recently pushed WG key. + case switchKey + + /// Monitor events. + case monitorEvent(_ event: TunnelMonitorEvent) + + /// Network reachability events. + case networkReachability(NetworkPath) + + /// Update the device private key, as per post-quantum protocols + case replaceDevicePrivateKey(PreSharedKey) + + /// Format command for log output. + func logFormat() -> String { + switch self { + case .start: + return "start" + case .stop: + return "stop" + case let .reconnect(nextRelay, stopTunnelMonitor): + switch nextRelay { + case .current: + return "reconnect(current, \(stopTunnelMonitor))" + case .random: + return "reconnect(random, \(stopTunnelMonitor))" + case let .preSelected(selectedRelay): + return "reconnect(\(selectedRelay.hostname), \(stopTunnelMonitor))" + } + case let .error(reason): + return "error(\(reason))" + case .notifyKeyRotated: + return "notifyKeyRotated" + case let .monitorEvent(event): + switch event { + case .connectionEstablished: + return "monitorEvent(connectionEstablished)" + case .connectionLost: + return "monitorEvent(connectionLost)" + } + case .networkReachability: + return "networkReachability" + case .switchKey: + return "switchKey" + case .replaceDevicePrivateKey: + return "replaceDevicePrivateKey" + } + } + } +} diff --git a/ios/PacketTunnelCoreTests/CommandChannelTests.swift b/ios/PacketTunnelCoreTests/CommandChannelTests.swift index dc622434b9..974eca29f2 100644 --- a/ios/PacketTunnelCoreTests/CommandChannelTests.swift +++ b/ios/PacketTunnelCoreTests/CommandChannelTests.swift @@ -11,7 +11,7 @@ import XCTest final class CommandChannelTests: XCTestCase { func testCoalescingReconnect() async { - let channel = CommandChannel() + let channel = PacketTunnelActor.CommandChannel() channel.send(.start(StartOptions(launchSource: .app))) channel.send(.reconnect(.random)) @@ -27,7 +27,7 @@ final class CommandChannelTests: XCTestCase { /// Test that stops cancels all preceding tasks. func testCoalescingStop() async { - let channel = CommandChannel() + let channel = PacketTunnelActor.CommandChannel() channel.send(.start(StartOptions(launchSource: .app))) channel.send(.reconnect(.random)) @@ -44,7 +44,7 @@ final class CommandChannelTests: XCTestCase { /// Test that iterations over the finished channel yield `nil`. func testFinishFlushingUnconsumedValues() async { - let channel = CommandChannel() + let channel = PacketTunnelActor.CommandChannel() channel.send(.stop) channel.finish() @@ -54,7 +54,7 @@ final class CommandChannelTests: XCTestCase { /// Test that the call to `finish()` ends the iteration that began prior to that. func testFinishEndsAsyncIterator() async throws { - let channel = CommandChannel() + let channel = PacketTunnelActor.CommandChannel() let expectFinish = expectation(description: "Call to finish()") let expectEndIteration = expectation(description: "Iteration over channel should end upon call to finish()") @@ -91,7 +91,7 @@ enum PrimitiveCommand: Equatable { case start, stop, reconnect(NextRelay), switchKey, other } -extension Command { +extension PacketTunnelActor.Command { var primitiveCommand: PrimitiveCommand { switch self { case .start: |
