summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2024-05-29 13:40:23 +0200
committerBug Magnet <marco.nikic@mullvad.net>2024-05-29 13:40:23 +0200
commit322bff609986d89257105affc653925a6957e181 (patch)
treeca2e5d509da16d9d02302bdf3f1e365268d11add
parent596286dffee268327265879469588f8760cd43a6 (diff)
parent36d5524202f59497e2a522fadeee67884f9450b8 (diff)
downloadmullvadvpn-322bff609986d89257105affc653925a6957e181.tar.xz
mullvadvpn-322bff609986d89257105affc653925a6957e181.zip
Merge branch 'ios-692-packettunnelactor-reducer-refactoring'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj32
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift330
-rw-r--r--ios/PacketTunnelCore/Actor/EventChannel.swift (renamed from ios/PacketTunnelCore/Actor/CommandChannel.swift)55
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift4
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift2
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor+KeyPolicy.swift4
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift4
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift2
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift12
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor.swift106
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift4
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift184
-rw-r--r--ios/PacketTunnelCoreTests/EventChannelTests.swift (renamed from ios/PacketTunnelCoreTests/CommandChannelTests.swift)35
13 files changed, 665 insertions, 109 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index e0f774528d..8e0a798948 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -44,6 +44,8 @@
449EBA262B975B9700DFA4EB /* PostQuantumKeyReceiving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449EBA252B975B9700DFA4EB /* PostQuantumKeyReceiving.swift */; };
44B02E3B2BC5732D008EDF34 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44B02E3A2BC5732D008EDF34 /* LoggingTests.swift */; };
44B02E3C2BC5B8A5008EDF34 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; };
+ 44B3C43A2BFE2C800079782C /* PacketTunnelActorReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44B3C4392BFE2C800079782C /* PacketTunnelActorReducer.swift */; };
+ 44B3C43D2C00CBBD0079782C /* PacketTunnelActorReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44B3C43C2C00CBBC0079782C /* PacketTunnelActorReducerTests.swift */; };
44BB5F972BE527F4002520EB /* TunnelState+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */; };
44BB5F982BE527F4002520EB /* TunnelState+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */; };
44DD7D242B6CFFD70005F67F /* StartTunnelOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */; };
@@ -110,7 +112,7 @@
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 /* PacketTunnelActorCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832282AC3DF1300EA2071 /* PacketTunnelActorCommand.swift */; };
- 5838322B2AC3EF9600EA2071 /* CommandChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838322A2AC3EF9600EA2071 /* CommandChannel.swift */; };
+ 5838322B2AC3EF9600EA2071 /* EventChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838322A2AC3EF9600EA2071 /* EventChannel.swift */; };
583D86482A2678DC0060D63B /* DeviceStateAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */; };
583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DA21325FA4B5C00318683 /* LocationDataSource.swift */; };
583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */; };
@@ -168,7 +170,7 @@
586C0D932B03D90700E7CDD7 /* ShadowsocksItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D922B03D90700E7CDD7 /* ShadowsocksItemIdentifier.swift */; };
586C0D952B03D92100E7CDD7 /* SocksItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D942B03D92100E7CDD7 /* SocksItemIdentifier.swift */; };
586C0D992B04E20200E7CDD7 /* AccessMethodViewModel+Persistent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D982B04E20200E7CDD7 /* AccessMethodViewModel+Persistent.swift */; };
- 586C14582AC463BB00245C01 /* CommandChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C14572AC463BB00245C01 /* CommandChannelTests.swift */; };
+ 586C14582AC463BB00245C01 /* EventChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C14572AC463BB00245C01 /* EventChannelTests.swift */; };
586C145A2AC4735F00245C01 /* PacketTunnelActor+Public.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */; };
586E54FB27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */; };
586E8DB82AAF4AC4007BF3DA /* Task+Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */; };
@@ -1402,6 +1404,8 @@
449EB9FE2B95FF2500DFA4EB /* AccountMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMock.swift; sourceTree = "<group>"; };
449EBA252B975B9700DFA4EB /* PostQuantumKeyReceiving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostQuantumKeyReceiving.swift; sourceTree = "<group>"; };
44B02E3A2BC5732D008EDF34 /* LoggingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = "<group>"; };
+ 44B3C4392BFE2C800079782C /* PacketTunnelActorReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorReducer.swift; sourceTree = "<group>"; };
+ 44B3C43C2C00CBBC0079782C /* PacketTunnelActorReducerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorReducerTests.swift; sourceTree = "<group>"; };
44BB5F962BE527F4002520EB /* TunnelState+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelState+UI.swift"; sourceTree = "<group>"; };
44BB5F992BE529FE002520EB /* TunnelStateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelStateTests.swift; sourceTree = "<group>"; };
44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTunnelOperationTests.swift; sourceTree = "<group>"; };
@@ -1503,7 +1507,7 @@
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 /* 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>"; };
+ 5838322A2AC3EF9600EA2071 /* EventChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventChannel.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>"; };
583E1E292848DF67004838B3 /* OperationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationObserverTests.swift; sourceTree = "<group>"; };
@@ -1573,7 +1577,7 @@
586C0D942B03D92100E7CDD7 /* SocksItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocksItemIdentifier.swift; sourceTree = "<group>"; };
586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentAccessMethod.swift; sourceTree = "<group>"; };
586C0D982B04E20200E7CDD7 /* AccessMethodViewModel+Persistent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessMethodViewModel+Persistent.swift"; sourceTree = "<group>"; };
- 586C14572AC463BB00245C01 /* CommandChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandChannelTests.swift; sourceTree = "<group>"; };
+ 586C14572AC463BB00245C01 /* EventChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventChannelTests.swift; sourceTree = "<group>"; };
586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+Public.swift"; sourceTree = "<group>"; };
586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTunnelProviderMessageOperation.swift; sourceTree = "<group>"; };
586E7A2C2A987689006DAB1B /* SettingsReaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsReaderProtocol.swift; sourceTree = "<group>"; };
@@ -2381,6 +2385,7 @@
440E9EFB2BDA97C600B1FD11 /* GeneralAPIs */,
440E9EF12BDA940500B1FD11 /* Notifications */,
440E9EF72BDA95AC00B1FD11 /* PacketTunnel */,
+ 44B3C43B2C00CB570079782C /* PacketTunnelCore */,
440E9F012BDA99FA00B1FD11 /* Protocols */,
440E9EFE2BDA991200B1FD11 /* RelayCacheTracker */,
440E9EFF2BDA995800B1FD11 /* TunnelManager */,
@@ -2568,6 +2573,14 @@
path = Protocols;
sourceTree = "<group>";
};
+ 44B3C43B2C00CB570079782C /* PacketTunnelCore */ = {
+ isa = PBXGroup;
+ children = (
+ 44B3C43C2C00CBBC0079782C /* PacketTunnelActorReducerTests.swift */,
+ );
+ path = PacketTunnelCore;
+ sourceTree = "<group>";
+ };
5802EBC32A8E447000E5CE4C /* Router */ = {
isa = PBXGroup;
children = (
@@ -3080,7 +3093,7 @@
58BDEBA02A9CA14B00F578F2 /* AnyTask.swift */,
58F3F3652AA086A400D3B0A4 /* AutoCancellingTask.swift */,
583832282AC3DF1300EA2071 /* PacketTunnelActorCommand.swift */,
- 5838322A2AC3EF9600EA2071 /* CommandChannel.swift */,
+ 5838322A2AC3EF9600EA2071 /* EventChannel.swift */,
583E60952A9F6D0800DC61EF /* ConfigurationBuilder.swift */,
580D6B892AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift */,
58CF95A12AD6F35800B59F5D /* ObservedState.swift */,
@@ -3102,6 +3115,7 @@
586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */,
58DDA18E2ABC32380039C360 /* Timings.swift */,
44DF8AC32BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift */,
+ 44B3C4392BFE2C800079782C /* PacketTunnelActorReducer.swift */,
);
path = Actor;
sourceTree = "<group>";
@@ -3356,7 +3370,7 @@
children = (
58EC067D2A8D2B0700BEB973 /* Mocks */,
7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */,
- 586C14572AC463BB00245C01 /* CommandChannelTests.swift */,
+ 586C14572AC463BB00245C01 /* EventChannelTests.swift */,
58FE25D32AA729B5003D1918 /* PacketTunnelActorTests.swift */,
58C7A46F2A8649ED0060C66F /* PingerTests.swift */,
5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */,
@@ -5286,6 +5300,7 @@
A9A5F9EA2ACB05160083449F /* Bundle+ProductVersion.swift in Sources */,
A9A5F9EB2ACB05160083449F /* CharacterSet+IPAddress.swift in Sources */,
F0D8825C2B04F70E00D3EF9A /* OutgoingConnectionData.swift in Sources */,
+ 44B3C43D2C00CBBD0079782C /* PacketTunnelActorReducerTests.swift in Sources */,
A9A5F9EC2ACB05160083449F /* CodingErrors+CustomErrorDescription.swift in Sources */,
A9A5F9ED2ACB05160083449F /* NSRegularExpression+IPAddress.swift in Sources */,
A9A5F9EE2ACB05160083449F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */,
@@ -5471,6 +5486,7 @@
58FE25D72AA72A8F003D1918 /* State.swift in Sources */,
58C7A4592A863FB90060C66F /* WgStats.swift in Sources */,
7AD0AA1F2AD6C8B900119E10 /* URLRequestProxyProtocol.swift in Sources */,
+ 44B3C43A2BFE2C800079782C /* PacketTunnelActorReducer.swift in Sources */,
7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */,
A95EEE362B722CD600A8A39B /* TunnelMonitorState.swift in Sources */,
58FE25DB2AA72A8F003D1918 /* StartOptions.swift in Sources */,
@@ -5482,7 +5498,7 @@
7AD0AA1D2AD6A86700119E10 /* PacketTunnelActorProtocol.swift in Sources */,
5826B6CB2ABD83E200B1CA13 /* PacketTunnelOptions.swift in Sources */,
586E8DB82AAF4AC4007BF3DA /* Task+Duration.swift in Sources */,
- 5838322B2AC3EF9600EA2071 /* CommandChannel.swift in Sources */,
+ 5838322B2AC3EF9600EA2071 /* EventChannel.swift in Sources */,
586C145A2AC4735F00245C01 /* PacketTunnelActor+Public.swift in Sources */,
58342C042AAB61FB003BA12D /* State+Extensions.swift in Sources */,
A95EEE382B722DFC00A8A39B /* PingStats.swift in Sources */,
@@ -5500,7 +5516,7 @@
files = (
7AD0AA1C2AD6A63F00119E10 /* PacketTunnelActorStub.swift in Sources */,
58EC067A2A8D208D00BEB973 /* TunnelDeviceInfoStub.swift in Sources */,
- 586C14582AC463BB00245C01 /* CommandChannelTests.swift in Sources */,
+ 586C14582AC463BB00245C01 /* EventChannelTests.swift in Sources */,
58EC067C2A8D2A0B00BEB973 /* NetworkCounters.swift in Sources */,
58FE25EC2AA77639003D1918 /* TunnelMonitorStub.swift in Sources */,
7A3FD1B82AD54AE60042BEA6 /* TimeServerProxy.swift in Sources */,
diff --git a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift
new file mode 100644
index 0000000000..a57d78bd39
--- /dev/null
+++ b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift
@@ -0,0 +1,330 @@
+//
+// PacketTunnelActorReducerTests.swift
+// MullvadVPNTests
+//
+// Created by Andrew Bulhak on 2024-04-29.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadTypes
+@testable import PacketTunnelCore
+import WireGuardKitTypes
+import XCTest
+
+final class PacketTunnelActorReducerTests: XCTestCase {
+ // test data
+ let selectedRelay = SelectedRelay(
+ endpoint: MullvadEndpoint(
+ ipv4Relay: IPv4Endpoint(ip: .loopback, port: 1300),
+ ipv4Gateway: .loopback,
+ ipv6Gateway: .loopback,
+ publicKey: PrivateKey().publicKey.rawValue
+ ),
+ hostname: "se-got",
+ location: Location(
+ country: "",
+ countryCode: "se",
+ city: "",
+ cityCode: "got",
+ latitude: 0,
+ longitude: 0
+ ), retryAttempts: 0
+ )
+ func makeConnectionData(keyPolicy: State.KeyPolicy = .useCurrent) -> State.ConnectionData {
+ State.ConnectionData(
+ selectedRelay: selectedRelay,
+ relayConstraints: RelayConstraints(),
+ keyPolicy: keyPolicy,
+ networkReachability: .reachable,
+ connectionAttemptCount: 0,
+ connectedEndpoint: selectedRelay.endpoint,
+ transportLayer: .udp,
+ remotePort: 12345,
+ isPostQuantum: false
+ )
+ }
+
+ // MARK: .start
+
+ func testHandleStartWithoutPreselectedRelay() {
+ // Given
+ var state = State.initial
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .start(StartOptions(launchSource: .app)))
+ // Then
+ XCTAssertEqual(effects, [
+ .startDefaultPathObserver,
+ .startTunnelMonitor,
+ .startConnection(.random),
+ ])
+ }
+
+ func testHandleStartWithPreselectedRelay() {
+ // Given
+ var state = State.initial
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(
+ &state,
+ .start(StartOptions(launchSource: .app, selectedRelay: selectedRelay))
+ )
+ // Then
+ XCTAssertEqual(effects, [
+ .startDefaultPathObserver,
+ .startTunnelMonitor,
+ .startConnection(.preSelected(selectedRelay)),
+ ])
+ }
+
+ // MARK: .stop
+
+ func testHandleStopFromConnected() {
+ // Given
+ let connectionData = makeConnectionData()
+ var state = State.connected(connectionData)
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .stop)
+ // Then
+ XCTAssertEqual(state, .disconnecting(connectionData))
+ XCTAssertEqual(effects, [
+ .stopTunnelMonitor,
+ .stopDefaultPathObserver,
+ .stopTunnelAdapter,
+ .setDisconnectedState,
+ ])
+ }
+
+ func testHandleStopFromConnecting() {
+ // Given
+ let connectionData = makeConnectionData()
+ var state = State.connecting(connectionData)
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .stop)
+ // Then
+ XCTAssertEqual(state, .disconnecting(connectionData))
+ XCTAssertEqual(effects, [
+ .stopTunnelMonitor,
+ .stopDefaultPathObserver,
+ .stopTunnelAdapter,
+ .setDisconnectedState,
+ ])
+ }
+
+ func testHandleStopFromReconnecting() {
+ // Given
+ let connectionData = makeConnectionData()
+ var state = State.reconnecting(connectionData)
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .stop)
+ // Then
+ XCTAssertEqual(state, .disconnecting(connectionData))
+ XCTAssertEqual(effects, [
+ .stopTunnelMonitor,
+ .stopDefaultPathObserver,
+ .stopTunnelAdapter,
+ .setDisconnectedState,
+ ])
+ }
+
+ func testHandleStopFromError() {
+ // Given
+ let blockingData = State.BlockingData(
+ reason: .accountExpired,
+ keyPolicy: .useCurrent,
+ networkReachability: .reachable,
+ priorState: .connected
+ )
+ var state = State.error(blockingData)
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .stop)
+
+ // Then
+ XCTAssertEqual(effects, [
+ .stopDefaultPathObserver,
+ .stopTunnelAdapter,
+ .setDisconnectedState,
+ ])
+ }
+
+ func testHandleStopFromUnconnectedStates() {
+ // Given
+ let states: [State] = [.initial, .disconnected]
+
+ for var state in states {
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .stop)
+
+ // Then
+ XCTAssertEqual(effects, [])
+ }
+ }
+
+ // MARK: .reconnect
+
+ func testHandleUserInitiatedReconnectFromConnectedStates() {
+ // Given
+ var state = State.connected(makeConnectionData())
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .reconnect(.current, reason: .userInitiated))
+
+ // Then
+ XCTAssertEqual(effects, [
+ .stopTunnelMonitor,
+ .restartConnection(.current, .userInitiated),
+ ])
+ }
+
+ func testHandleConnectionLossReconnectFromConnectedStates() {
+ // Given
+ var state = State.connected(makeConnectionData())
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .reconnect(.random, reason: .connectionLoss))
+
+ // Then
+ XCTAssertEqual(effects, [
+ .restartConnection(.random, .connectionLoss),
+ ])
+ }
+
+ func testHandleReconnectFromDisconnectedIsNoOp() {
+ // Given
+ var state = State.disconnected
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .reconnect(.random, reason: .connectionLoss))
+
+ // Then
+ XCTAssertEqual(effects, [])
+ }
+
+ func testHandleConnectionLossReconnectFromPQKeyNegotiation() {
+ // Given
+ var state = State.negotiatingPostQuantumKey(makeConnectionData(), PrivateKey())
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .reconnect(.random, reason: .connectionLoss))
+
+ // Then
+ XCTAssertEqual(effects, [.restartConnection(.random, .connectionLoss)])
+ }
+
+ func testHandleUserReconnectFromPQKeyNegotiation() {
+ // Given
+ var state = State.negotiatingPostQuantumKey(makeConnectionData(), PrivateKey())
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .reconnect(.random, reason: .userInitiated))
+
+ // Then
+ XCTAssertEqual(effects, [
+ .stopTunnelMonitor,
+ .restartConnection(.random, .userInitiated),
+ ])
+ }
+
+ // MARK: .error
+
+ func testHandleError() {
+ // Given
+ var state = State.connected(makeConnectionData())
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .error(.deviceRevoked))
+
+ // then
+ XCTAssertEqual(effects, [
+ .configureForErrorState(.deviceRevoked),
+ ])
+ }
+
+ // MARK: .notifyKeyRotated
+
+ func testHandleNotifyKeyRotatedWhileUsingCurrentKey() {
+ // Given
+ var state = State.connected(makeConnectionData(keyPolicy: .useCurrent))
+ let date = Date()
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .notifyKeyRotated(date))
+
+ // then
+ XCTAssertEqual(effects, [
+ .cacheActiveKey(date),
+ ])
+ }
+
+ func testHandleNotifyKeyRotatedWhileUsingPriorKey() {
+ // Given
+ let keyPolicy = State.KeyPolicy.usePrior(PrivateKey(), AutoCancellingTask(Task(operation: {})))
+ var state = State.connected(makeConnectionData(keyPolicy: keyPolicy))
+ let date = Date()
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .notifyKeyRotated(date))
+
+ // then
+ XCTAssertEqual(effects, [])
+ }
+
+ // MARK: .switchKey
+
+ func testHandleSwitchKeyFromUseCurrent() {
+ // Given
+ var state = State.connected(makeConnectionData(keyPolicy: .useCurrent))
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .switchKey)
+
+ // then
+ XCTAssertEqual(effects, [])
+ }
+
+ func testHandleSwitchKeyFromUsePrior() {
+ // Given
+ let keyPolicy = State.KeyPolicy.usePrior(PrivateKey(), AutoCancellingTask(Task(operation: {})))
+ var state = State.connected(makeConnectionData(keyPolicy: keyPolicy))
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .switchKey)
+
+ // then
+ XCTAssertEqual(state.keyPolicy, State.KeyPolicy.useCurrent)
+ XCTAssertEqual(effects, [
+ .reconnect(.random),
+ ])
+ }
+
+ // MARK: .monitorEvent
+
+ func testHandleMonitorEvent_ConnectionEstablishedWhileConnecting() {
+ // Given
+ var connectionData = makeConnectionData()
+ connectionData.connectionAttemptCount = 2
+ var state = State.connecting(connectionData)
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .monitorEvent(.connectionEstablished))
+
+ // Then
+ var expectedConnectionData = connectionData
+ expectedConnectionData.connectionAttemptCount = 0
+ XCTAssertEqual(state, .connected(expectedConnectionData))
+ XCTAssertEqual(effects, [])
+ }
+
+ func testHandleMonitorEvent_ConnectionLostWhileConnected() {
+ // Given
+ let connectionData = makeConnectionData()
+ var state = State.connected(connectionData)
+
+ // When
+ let effects = PacketTunnelActor.Reducer.reduce(&state, .monitorEvent(.connectionLost))
+
+ // Then
+ XCTAssertEqual(effects, [
+ .restartConnection(.random, .connectionLoss),
+ ])
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/CommandChannel.swift b/ios/PacketTunnelCore/Actor/EventChannel.swift
index e159100c34..a5aedcb198 100644
--- a/ios/PacketTunnelCore/Actor/CommandChannel.swift
+++ b/ios/PacketTunnelCore/Actor/EventChannel.swift
@@ -1,9 +1,10 @@
//
-// CommandChannel.swift
+// EventChannel.swift
// PacketTunnelCore
//
// Created by pronebird on 27/09/2023.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+// Formerly known as CommandChannel
//
import Foundation
@@ -11,8 +12,8 @@ import Foundation
/**
Sync-to-async ordered coalescing channel with unbound buffering.
- Publishers send commands over the channel to pass work to consumer. Received commands are buffered, until requested by consumer and coalesced just
- before consumption.
+ Publishers send events over the channel to pass work to consumer. Received events
+ are buffered, until requested by consumer and coalesced just before consumption.
- Multiple consumers are possible but the actor is really expected to be the only consumer.
- Internally, the channel acquires a lock, so you can assume FIFO ordering unless you publish values simultaneously from multiple concurrent queues.
@@ -20,17 +21,17 @@ import Foundation
### Example
```
- let channel = CommandChannel()
+ let channel = EventChannel()
channel.send(.stop)
```
- Consuming commands can be implemented using a for-await loop. Note that using a loop should also serialize the command handling as the next command will not
+ Consuming events can be implemented using a for-await loop. Note that using a loop should also serialize the event handling as the next event will not
be consumed until the body of the loop completes the iteration.
```
Task.detached {
- for await command in channel {
- await handleMyCommand(command)
+ for await event in channel {
+ await handleMyEvent(event)
}
}
```
@@ -42,14 +43,14 @@ import Foundation
channel.send(.stop)
channel.sendEnd()
- let allReceivedCommands = channel
+ let allReceivedEvents = channel
.map { "\($0)" }
.reduce(into: [String]()) { $0.append($1) }
```
*/
extension PacketTunnelActor {
- final class CommandChannel: @unchecked Sendable {
- typealias Command = PacketTunnelActor.Command
+ final class EventChannel: @unchecked Sendable {
+ typealias Event = PacketTunnelActor.Event
private enum State {
/// Channel is active and running.
case active
@@ -64,12 +65,12 @@ extension PacketTunnelActor {
case finished
}
- /// A buffer of commands received but not consumed yet.
- private var buffer: [Command] = []
+ /// A buffer of events received but not consumed yet.
+ private var buffer: [Event] = []
/// 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 pendingContinuations: [CheckedContinuation<Event?, Never>] = []
private var state: State = .active
private var stateLock = NSLock()
@@ -81,10 +82,10 @@ extension PacketTunnelActor {
finish()
}
- /// Send command to consumer.
+ /// Send event to consumer.
///
- /// - Parameter value: a new command.
- func send(_ value: Command) {
+ /// - Parameter value: a new event.
+ func send(_ value: Event) {
stateLock.withLock {
guard case .active = state else { return }
@@ -112,7 +113,7 @@ extension PacketTunnelActor {
}
}
- /// Flush buffered commands and resume all pending continuations sending them `nil` to mark the end of iteration.
+ /// Flush buffered events and resume all pending continuations sending them `nil` to mark the end of iteration.
func finish() {
stateLock.withLock {
switch state {
@@ -137,15 +138,15 @@ extension PacketTunnelActor {
}
/// 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? {
+ /// Returns `nil` if the buffer is empty, otherwise if attempts to coalesce buffered events before consuming the first comand in the list.
+ private func consumeFirst() -> Event? {
guard !buffer.isEmpty else { return nil }
coalesce()
return buffer.removeFirst()
}
- /// Coalesce buffered commands to prevent future execution when the outcome is considered to be similar.
+ /// Coalesce buffered events to prevent future execution when the outcome is considered to be similar.
/// Mutates internal `buffer`.
private func coalesce() {
var i = buffer.count - 1
@@ -155,14 +156,14 @@ extension PacketTunnelActor {
assert(i < buffer.count)
let current = buffer[i]
- // Remove all preceding commands when encountered "stop".
+ // Remove all preceding events 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.
+ // This will rearrange the event 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() {
@@ -177,7 +178,7 @@ extension PacketTunnelActor {
}
}
- private func next() async -> Command? {
+ private func next() async -> Event? {
return await withCheckedContinuation { continuation in
stateLock.withLock {
switch state {
@@ -206,12 +207,12 @@ extension PacketTunnelActor {
}
}
-extension PacketTunnelActor.CommandChannel: AsyncSequence {
- typealias Element = Command
+extension PacketTunnelActor.EventChannel: AsyncSequence {
+ typealias Element = Event
struct AsyncIterator: AsyncIteratorProtocol {
- let channel: PacketTunnelActor.CommandChannel
- func next() async -> Command? {
+ let channel: PacketTunnelActor.EventChannel
+ func next() async -> Event? {
return await channel.next()
}
}
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift
index 84d34d2650..aec5b94dec 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift
@@ -13,7 +13,7 @@ extension PacketTunnelActor {
func setTunnelMonitorEventHandler() {
tunnelMonitor.onEvent = { [weak self] event in
/// Dispatch tunnel monitor events via command channel to guarantee the order of execution.
- self?.commandChannel.send(.monitorEvent(event))
+ self?.eventChannel.send(.monitorEvent(event))
}
}
@@ -51,7 +51,7 @@ extension PacketTunnelActor {
private func onHandleConnectionRecovery() async {
switch state {
case .connecting, .reconnecting, .connected:
- commandChannel.send(.reconnect(.random, reason: .connectionLoss))
+ eventChannel.send(.reconnect(.random, reason: .connectionLoss))
case .initial, .disconnected, .disconnecting, .error, .negotiatingPostQuantumKey:
break
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift
index fd88ea94e4..8f94dd7086 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift
@@ -151,7 +151,7 @@ extension PacketTunnelActor {
try await Task.sleepUsingContinuousClock(for: timings.bootRecoveryPeriodicity)
// Schedule task to reconnect.
- commandChannel.send(.reconnect(.random))
+ eventChannel.send(.reconnect(.random))
}
}
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+KeyPolicy.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+KeyPolicy.swift
index 9181a73edf..cea84d4f21 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+KeyPolicy.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+KeyPolicy.swift
@@ -46,7 +46,7 @@ extension PacketTunnelActor {
*/
func switchToCurrentKey() {
if switchToCurrentKeyInner() {
- commandChannel.send(.reconnect(.random))
+ eventChannel.send(.reconnect(.random))
}
}
@@ -65,7 +65,7 @@ extension PacketTunnelActor {
try await Task.sleepUsingContinuousClock(for: timings.wgKeyPropagationDelay)
// Enqueue task to change key policy.
- commandChannel.send(.switchKey)
+ eventChannel.send(.switchKey)
}
return AutoCancellingTask(task)
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift
index e60a0081ff..c475e3d798 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift
@@ -18,11 +18,11 @@ extension PacketTunnelActor {
logger.trace("Start default path observer.")
defaultPathObserver.start { [weak self] networkPath in
- self?.commandChannel.send(.networkReachability(networkPath))
+ self?.eventChannel.send(.networkReachability(networkPath))
}
if notifyObserverWithCurrentPath, let currentPath = defaultPathObserver.defaultPath {
- commandChannel.send(.networkReachability(currentPath))
+ eventChannel.send(.networkReachability(currentPath))
}
}
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift
index 53066ce437..d721f571de 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift
@@ -55,7 +55,7 @@ extension PacketTunnelActor {
logger.error("Could not create connection state in PostQuantumConnect")
let nextRelay: NextRelay = (state.connectionData?.selectedRelay).map { .preSelected($0) } ?? .current
- commandChannel.send(.reconnect(nextRelay))
+ eventChannel.send(.reconnect(nextRelay))
return
}
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift
index 209a6041d5..5457757327 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift
@@ -23,14 +23,14 @@ extension PacketTunnelActor {
- Parameter options: start options.
*/
nonisolated public func start(options: StartOptions) {
- commandChannel.send(.start(options))
+ eventChannel.send(.start(options))
}
/**
Tell actor to stop the tunnel.
*/
nonisolated public func stop() {
- commandChannel.send(.stop)
+ eventChannel.send(.stop)
}
/**
@@ -39,7 +39,7 @@ extension PacketTunnelActor {
- Parameter nextRelay: next relay to connect to.
*/
public nonisolated func reconnect(to nextRelay: NextRelay) {
- commandChannel.send(.reconnect(nextRelay))
+ eventChannel.send(.reconnect(nextRelay))
}
/**
@@ -48,7 +48,7 @@ extension PacketTunnelActor {
- Parameter date: date when last key rotation took place.
*/
nonisolated public func notifyKeyRotation(date: Date?) {
- commandChannel.send(.notifyKeyRotated(date))
+ eventChannel.send(.notifyKeyRotated(date))
}
/**
@@ -57,13 +57,13 @@ extension PacketTunnelActor {
*/
nonisolated public func replacePreSharedKey(_ key: PreSharedKey, ephemeralKey: PrivateKey) {
- commandChannel.send(.replaceDevicePrivateKey(key, ephemeralKey: ephemeralKey))
+ eventChannel.send(.replaceDevicePrivateKey(key, ephemeralKey: ephemeralKey))
}
/**
Tell actor to enter error state.
*/
nonisolated public func setErrorState(reason: BlockedStateReason) {
- commandChannel.send(.error(reason))
+ eventChannel.send(.error(reason))
}
}
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
index 153c1c0430..360bccb46f 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
@@ -16,13 +16,13 @@ import WireGuardKitTypes
/**
Packet tunnel state machine implemented as an actor.
- - Actor receives commands for execution over the `CommandChannel`.
+ - Actor receives events for execution over the `EventChannel`.
- - Commands are consumed in a detached task via for-await loop over the channel. Each command, once received, is executed in its entirety before the next
- command is processed. See the implementation of `consumeCommands()` which is the central task dispatcher inside of actor.
+ - Events are consumed in a detached task via for-await loop over the channel. Each event, once received, is executed in its entirety before the next
+ event is processed. See the implementation of `consumeEvents()` which is the central task dispatcher inside of actor.
- - Most of calls that actor performs suspend for a very short amount of time. `CommandChannel` proactively discards unwanted tasks as they arrive to prevent
- future execution, such as repeating commands to reconnect are coalesced and all commands prior to stop are discarded entirely as the outcome would be the
+ - Most of calls that actor performs suspend for a very short amount of time. `EventChannel` proactively discards unwanted tasks as they arrive to prevent
+ future execution, such as repeating commands to reconnect are coalesced and all events prior to stop are discarded entirely as the outcome would be the
same anyway.
*/
public actor PacketTunnelActor {
@@ -47,7 +47,7 @@ public actor PacketTunnelActor {
let settingsReader: SettingsReaderProtocol
let protocolObfuscator: ProtocolObfuscation
- nonisolated let commandChannel = CommandChannel()
+ nonisolated let eventChannel = EventChannel()
public init(
timings: PacketTunnelActorTimings,
@@ -68,64 +68,88 @@ public actor PacketTunnelActor {
self.settingsReader = settingsReader
self.protocolObfuscator = protocolObfuscator
- consumeCommands(channel: commandChannel)
+ consumeEvents(channel: eventChannel)
}
deinit {
- commandChannel.finish()
+ eventChannel.finish()
}
/**
- Spawn a detached task that consumes commands from the channel indefinitely until the channel is closed.
- Commands are processed one at a time, so no suspensions should affect the order of execution and thus guarantee transactional execution.
+ Spawn a detached task that consumes events from the channel indefinitely until the channel is closed.
+ Events are processed one at a time, so no suspensions should affect the order of execution and thus guarantee transactional execution.
- - Parameter channel: command channel.
+ - Parameter channel: event channel.
*/
- private nonisolated func consumeCommands(channel: CommandChannel) {
+ private nonisolated func consumeEvents(channel: EventChannel) {
Task.detached { [weak self] in
- for await command in channel {
+ for await event in channel {
guard let self else { return }
- self.logger.debug("Received command: \(command.logFormat())")
+ self.logger.debug("Received event: \(event.logFormat())")
- switch command {
- case let .start(options):
- await start(options: options)
+ let effects = await self.runReducer(event)
- case .stop:
- await stop()
-
- case let .reconnect(nextRelay, reason):
- await reconnect(to: nextRelay, reason: reason)
-
- case let .error(reason):
- await setErrorStateInternal(with: reason)
-
- case let .notifyKeyRotated(date):
- await cacheActiveKey(lastKeyRotation: date)
-
- case .switchKey:
- await switchToCurrentKey()
-
- case let .monitorEvent(event):
- await handleMonitorEvent(event)
-
- case let .networkReachability(defaultPath):
- await handleDefaultPathChange(defaultPath)
-
- case let .replaceDevicePrivateKey(preSharedKey, ephemeralKey):
- await postQuantumConnect(with: preSharedKey, privateKey: ephemeralKey)
+ for effect in effects {
+ await executeEffect(effect)
}
}
}
}
+
+ func executeEffect(_ effect: Effect) async {
+ switch effect {
+ case .startDefaultPathObserver:
+ startDefaultPathObserver()
+ case .stopDefaultPathObserver:
+ stopDefaultPathObserver()
+ case .startTunnelMonitor:
+ setTunnelMonitorEventHandler()
+ case .stopTunnelMonitor:
+ tunnelMonitor.stop()
+ case let .updateTunnelMonitorPath(networkPath):
+ handleDefaultPathChange(networkPath)
+ case let .startConnection(nextRelay):
+ do {
+ try await tryStart(nextRelay: nextRelay)
+ } catch {
+ logger.error(error: error, message: "Failed to start the tunnel.")
+ await setErrorStateInternal(with: error)
+ }
+ case let .restartConnection(nextRelay, reason):
+ do {
+ try await tryStart(nextRelay: nextRelay, reason: reason)
+ } catch {
+ logger.error(error: error, message: "Failed to reconnect the tunnel.")
+ await setErrorStateInternal(with: error)
+ }
+ case let .reconnect(nextRelay):
+ eventChannel.send(.reconnect(nextRelay))
+ case .stopTunnelAdapter:
+ do {
+ try await tunnelAdapter.stop()
+ } catch {
+ logger.error(error: error, message: "Failed to stop adapter.")
+ }
+ state = .disconnected
+ case let .configureForErrorState(reason):
+ await setErrorStateInternal(with: reason)
+
+ case let .cacheActiveKey(lastKeyRotation):
+ cacheActiveKey(lastKeyRotation: lastKeyRotation)
+ case let .postQuantumConnect(key, privateKey: privateKey):
+ await postQuantumConnect(with: key, privateKey: privateKey)
+ case .setDisconnectedState:
+ self.state = .disconnected
+ }
+ }
}
// MARK: -
extension PacketTunnelActor {
/// Describes the reason for reconnection request.
- enum ReconnectReason {
+ enum ReconnectReason: Equatable {
/// Initiated by user.
case userInitiated
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift
index c4bd2c314b..024431d993 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift
@@ -10,8 +10,8 @@ import Foundation
import WireGuardKitTypes
extension PacketTunnelActor {
- /// Describes action that actor can perform.
- enum Command {
+ /// Describes events that the state machine handles. These can be user commands or non-user-initiated events
+ enum Event {
/// Start tunnel.
case start(StartOptions)
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift
new file mode 100644
index 0000000000..338677e6c9
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift
@@ -0,0 +1,184 @@
+//
+// PacketTunnelActorReducer.swift
+// PacketTunnelCore
+//
+// Created by Andrew Bulhak on 2024-05-22.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+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 {
+ case startDefaultPathObserver
+ case stopDefaultPathObserver
+ case startTunnelMonitor
+ case stopTunnelMonitor
+ case updateTunnelMonitorPath(NetworkPath)
+ case startConnection(NextRelay)
+ case restartConnection(NextRelay, ReconnectReason)
+ // trigger a reconnect, which becomes several effects depending on the state
+ case reconnect(NextRelay)
+ case stopTunnelAdapter
+ case configureForErrorState(BlockedStateReason)
+ case cacheActiveKey(Date?)
+ case postQuantumConnect(PreSharedKey, privateKey: PrivateKey)
+ // acknowledge that the disconnection process has concluded, go to .disconnected.
+ case setDisconnectedState
+
+ // 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.status == rp.status
+ case let (.startConnection(nr0), .startConnection(nr1)): nr0 == nr1
+ case let (.restartConnection(nr0, rr0), .restartConnection(nr1, rr1)): nr0 == nr1 && rr0 == rr1
+ case let (.reconnect(nr0), .reconnect(nr1)): nr0 == nr1
+ case (.stopTunnelAdapter, .stopTunnelAdapter): true
+ case let (.configureForErrorState(r0), .configureForErrorState(r1)): r0 == r1
+ case let (.cacheActiveKey(d0), .cacheActiveKey(d1)): d0 == d1
+ case let (.postQuantumConnect(psk0, privateKey: pk0), .postQuantumConnect(psk1, privateKey: pk1)): psk0 ==
+ psk1 && pk0 == pk1
+ case (.setDisconnectedState, .setDisconnectedState): true
+ default: false
+ }
+ }
+ }
+
+ struct Reducer {
+ static func reduce(_ state: inout State, _ event: Event) -> [Effect] {
+ switch event {
+ case let .start(options):
+ guard case .initial = state else { return [] }
+ return [
+ .startDefaultPathObserver,
+ .startTunnelMonitor,
+ .startConnection(options.selectedRelay.map { .preSelected($0) } ?? .random),
+ ]
+ case .stop:
+ return subreducerForStop(&state)
+
+ case let .reconnect(nextRelay, reason: reason):
+ return subreducerForReconnect(state, reason, nextRelay)
+
+ case let .error(reason):
+ // the transition from error to blocked state currently has side-effects, so will be handled as an effect for now.
+ return [.configureForErrorState(reason)]
+
+ case let .notifyKeyRotated(lastKeyRotation):
+ // the cacheActiveKey operation is currently effectful, starting a key-switch task within the mutation of state, so this is entirely done in an effect. Perhaps teasing effects out of state mutation is a future refactoring?
+ guard state.keyPolicy == .useCurrent else { return [] }
+ return [.cacheActiveKey(lastKeyRotation)]
+
+ case .switchKey:
+ return subreducerForSwitchKey(&state)
+
+ case let .monitorEvent(event):
+ return subreducerForTunnelMonitorEvent(event, &state)
+
+ case let .networkReachability(defaultPath):
+ let newReachability = defaultPath.networkReachability
+ state.mutateAssociatedData { $0.networkReachability = newReachability }
+ return [.updateTunnelMonitorPath(defaultPath)]
+
+ case let .replaceDevicePrivateKey(key, ephemeralKey: ephemeralKey):
+ return [.postQuantumConnect(key, privateKey: ephemeralKey)]
+ }
+ }
+
+ // Parts of the reducer path broken out for specific incoming events
+
+ fileprivate static func subreducerForStop(_ state: inout State) -> [PacketTunnelActor.Effect] {
+ // a call of the reducer produces one state transition and a sequence of effects. In the app, a stop transitions to .disconnecting, shuts down various processes, and finally transitions to .disconnected. We currently do this by having an effect which acknowledges the completion of disconnection and just sets the state. This is a bit messy, and could possibly do with some rethinking.
+ switch state {
+ case let .connected(connState), let .connecting(connState), let .reconnecting(connState),
+ let .negotiatingPostQuantumKey(connState, _):
+ state = .disconnecting(connState)
+ return [
+ .stopTunnelMonitor,
+ .stopDefaultPathObserver,
+ .stopTunnelAdapter,
+ .setDisconnectedState,
+ ]
+ case .error:
+ return [
+ .stopDefaultPathObserver,
+ .stopTunnelAdapter,
+ .setDisconnectedState,
+ ]
+
+ case .initial, .disconnected:
+ return []
+
+ case .disconnecting:
+ assertionFailure("stop(): out of order execution.")
+ return []
+ }
+ }
+
+ fileprivate static func subreducerForReconnect(
+ _ state: State,
+ _ reason: PacketTunnelActor.ReconnectReason,
+ _ nextRelay: NextRelay
+ ) -> [PacketTunnelActor.Effect] {
+ switch state {
+ case .disconnected, .disconnecting, .initial:
+ // There is no connection monitoring going on when exchanging keys.
+ // The procedure starts from scratch for each reconnection attempts.
+ return []
+ case .connecting, .connected, .reconnecting, .error, .negotiatingPostQuantumKey:
+ if reason == .userInitiated {
+ return [.stopTunnelMonitor, .restartConnection(nextRelay, reason)]
+ } else {
+ return [.restartConnection(nextRelay, reason)]
+ }
+ }
+ }
+
+ fileprivate static func subreducerForSwitchKey(_ state: inout State) -> [PacketTunnelActor.Effect] {
+ let oldKeyPolicy = state.keyPolicy
+ state.mutateKeyPolicy { keyPolicy in
+ if case .usePrior = keyPolicy {
+ keyPolicy = .useCurrent
+ }
+ }
+ if case .error = state { return [] }
+ return state.keyPolicy != oldKeyPolicy ? [.reconnect(.random)] : []
+ }
+
+ fileprivate static func subreducerForTunnelMonitorEvent(
+ _ event: TunnelMonitorEvent,
+ _ state: inout State
+ ) -> [PacketTunnelActor.Effect] {
+ switch event {
+ case .connectionEstablished:
+ switch state {
+ case var .connecting(connState), var .reconnecting(connState):
+ // Reset connection attempt once successfully connected.
+ connState.connectionAttemptCount = 0
+ state = .connected(connState)
+
+ case .initial, .connected, .disconnecting, .disconnected, .error, .negotiatingPostQuantumKey:
+ break
+ }
+ return []
+ case .connectionLost:
+ switch state {
+ case .connecting, .reconnecting, .connected:
+ return [.restartConnection(.random, .connectionLoss)]
+ case .initial, .disconnected, .disconnecting, .error, .negotiatingPostQuantumKey:
+ return []
+ }
+ }
+ }
+ }
+
+ func runReducer(_ event: Event) -> [Effect] {
+ PacketTunnelActor.Reducer.reduce(&state, event)
+ }
+}
diff --git a/ios/PacketTunnelCoreTests/CommandChannelTests.swift b/ios/PacketTunnelCoreTests/EventChannelTests.swift
index 974eca29f2..b6f50b3d0a 100644
--- a/ios/PacketTunnelCoreTests/CommandChannelTests.swift
+++ b/ios/PacketTunnelCoreTests/EventChannelTests.swift
@@ -1,17 +1,18 @@
//
-// CommandChannelTests.swift
+// EventChannelTests.swift
// PacketTunnelCoreTests
//
// Created by pronebird on 27/09/2023.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+// Formerly known as CommandChannelTests
//
@testable import PacketTunnelCore
import XCTest
-final class CommandChannelTests: XCTestCase {
+final class EventChannelTests: XCTestCase {
func testCoalescingReconnect() async {
- let channel = PacketTunnelActor.CommandChannel()
+ let channel = PacketTunnelActor.EventChannel()
channel.send(.start(StartOptions(launchSource: .app)))
channel.send(.reconnect(.random))
@@ -20,14 +21,14 @@ final class CommandChannelTests: XCTestCase {
channel.send(.reconnect(.current))
channel.sendEnd()
- let commands = await channel.map { $0.primitiveCommand }.collect()
+ let events = await channel.map { $0.primitiveCommand }.collect()
- XCTAssertEqual(commands, [.start, .switchKey, .reconnect(.current)])
+ XCTAssertEqual(events, [.start, .switchKey, .reconnect(.current)])
}
/// Test that stops cancels all preceding tasks.
func testCoalescingStop() async {
- let channel = PacketTunnelActor.CommandChannel()
+ let channel = PacketTunnelActor.EventChannel()
channel.send(.start(StartOptions(launchSource: .app)))
channel.send(.reconnect(.random))
@@ -37,14 +38,14 @@ final class CommandChannelTests: XCTestCase {
channel.send(.switchKey)
channel.sendEnd()
- let commands = await channel.map { $0.primitiveCommand }.collect()
+ let events = await channel.map { $0.primitiveCommand }.collect()
- XCTAssertEqual(commands, [.stop, .switchKey])
+ XCTAssertEqual(events, [.stop, .switchKey])
}
/// Test that iterations over the finished channel yield `nil`.
func testFinishFlushingUnconsumedValues() async {
- let channel = PacketTunnelActor.CommandChannel()
+ let channel = PacketTunnelActor.EventChannel()
channel.send(.stop)
channel.finish()
@@ -54,14 +55,14 @@ final class CommandChannelTests: XCTestCase {
/// Test that the call to `finish()` ends the iteration that began prior to that.
func testFinishEndsAsyncIterator() async throws {
- let channel = PacketTunnelActor.CommandChannel()
+ let channel = PacketTunnelActor.EventChannel()
let expectFinish = expectation(description: "Call to finish()")
let expectEndIteration = expectation(description: "Iteration over channel should end upon call to finish()")
- // Start iterating over commands in channel. The for-await loop should suspend the continuation.
+ // Start iterating over events in channel. The for-await loop should suspend the continuation.
Task {
- for await command in channel {
- print(command)
+ for await event in channel {
+ print(event)
}
expectEndIteration.fulfill()
@@ -86,13 +87,13 @@ extension AsyncSequence {
}
}
-/// Primitive version of `Command` that can be used in tests and easily compared against.
-enum PrimitiveCommand: Equatable {
+/// Simplified version of `Event` that can be used in tests and easily compared against.
+enum SimplifiedEvent: Equatable {
case start, stop, reconnect(NextRelay), switchKey, other
}
-extension PacketTunnelActor.Command {
- var primitiveCommand: PrimitiveCommand {
+extension PacketTunnelActor.Event {
+ var primitiveCommand: SimplifiedEvent {
switch self {
case .start:
return .start