summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2023-09-22 12:13:06 +0200
committerBug Magnet <marco.nikic@mullvad.net>2023-10-09 11:53:29 +0200
commitf29c4f82dfe6c98dc90b639e62bcb745db434cd9 (patch)
tree1468726fd34eb0e15d1608bf8fda419ba9fbfb07
parent817866f1843b6b9366153fdfa31288af9e80c26b (diff)
downloadmullvadvpn-f29c4f82dfe6c98dc90b639e62bcb745db434cd9.tar.xz
mullvadvpn-f29c4f82dfe6c98dc90b639e62bcb745db434cd9.zip
PacketTunnel: introduce proper state and blocked state
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj336
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift2
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift10
-rw-r--r--ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift12
-rw-r--r--ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift61
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState.swift9
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift18
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift2
-rw-r--r--ios/PacketTunnel/DeviceCheck/DeviceCheck.swift70
-rw-r--r--ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift2
-rw-r--r--ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift2
-rw-r--r--ios/PacketTunnel/MullvadEndpoint+WgEndpoint.swift23
-rw-r--r--ios/PacketTunnel/PacketTunnelConfiguration.swift74
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift786
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/AppMessageHandler.swift82
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift65
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/DeviceCheck+BlockedStateReason.swift25
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/DeviceChecker.swift57
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/NEProviderStopReason+Debug.swift (renamed from ios/PacketTunnel/NEProviderStopReason+Debug.swift)0
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/PacketTunnelPathObserver.swift (renamed from ios/PacketTunnel/PacketTunnelPathObserver.swift)0
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift266
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift28
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift75
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/State+Extensions.swift57
-rw-r--r--ios/PacketTunnel/WgAdapterDeviceInfo.swift85
-rw-r--r--ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift151
-rw-r--r--ios/PacketTunnel/WireGuardAdapter/WireGuardAdapter+Async.swift48
-rw-r--r--ios/PacketTunnel/WireGuardAdapter/WireGuardAdapterError+Localization.swift (renamed from ios/PacketTunnel/WireGuardAdapterError+Localization.swift)0
-rw-r--r--ios/PacketTunnel/WireGuardAdapter/WireGuardLogLevel+Logging.swift (renamed from ios/PacketTunnel/WireGuardLogLevel+Logging.swift)0
-rw-r--r--ios/PacketTunnelCore/Actor/Actor+ConnectionMonitoring.swift74
-rw-r--r--ios/PacketTunnelCore/Actor/Actor+ErrorState.swift148
-rw-r--r--ios/PacketTunnelCore/Actor/Actor+Extensions.swift54
-rw-r--r--ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift175
-rw-r--r--ios/PacketTunnelCore/Actor/Actor+NetworkReachability.swift79
-rw-r--r--ios/PacketTunnelCore/Actor/Actor+Public.swift59
-rw-r--r--ios/PacketTunnelCore/Actor/Actor+SleepCycle.swift29
-rw-r--r--ios/PacketTunnelCore/Actor/Actor.swift357
-rw-r--r--ios/PacketTunnelCore/Actor/AnyTask.swift17
-rw-r--r--ios/PacketTunnelCore/Actor/AutoCancellingTask.swift26
-rw-r--r--ios/PacketTunnelCore/Actor/Command.swift72
-rw-r--r--ios/PacketTunnelCore/Actor/CommandChannel.swift219
-rw-r--r--ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift54
-rw-r--r--ios/PacketTunnelCore/Actor/NetworkPath+NetworkReachability.swift28
-rw-r--r--ios/PacketTunnelCore/Actor/Protocols/BlockedStateErrorMapperProtocol.swift15
-rw-r--r--ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift17
-rw-r--r--ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift60
-rw-r--r--ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift38
-rw-r--r--ios/PacketTunnelCore/Actor/StartOptions.swift (renamed from ios/PacketTunnel/StartOptions.swift)21
-rw-r--r--ios/PacketTunnelCore/Actor/State+Extensions.swift112
-rw-r--r--ios/PacketTunnelCore/Actor/State.swift215
-rw-r--r--ios/PacketTunnelCore/Actor/Task+Duration.swift56
-rw-r--r--ios/PacketTunnelCore/Actor/Timings.swift28
-rw-r--r--ios/PacketTunnelCore/IPC/PacketTunnelErrorWrapper.swift49
-rw-r--r--ios/PacketTunnelCore/IPC/PacketTunnelStatus.swift87
-rw-r--r--ios/PacketTunnelCore/Pinger/Pinger.swift1
-rw-r--r--ios/PacketTunnelCore/Pinger/PingerProtocol.swift9
-rw-r--r--ios/PacketTunnelCore/TunnelMonitor/DefaultPathObserverProtocol.swift3
-rw-r--r--ios/PacketTunnelCore/TunnelMonitor/TunnelDeviceInfoProtocol.swift1
-rw-r--r--ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift11
-rw-r--r--ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift6
-rw-r--r--ios/PacketTunnelCore/URLRequestProxy/URLRequestProxy.swift24
-rw-r--r--ios/PacketTunnelCoreTests/ActorTests.swift102
-rw-r--r--ios/PacketTunnelCoreTests/CommandChannelTests.swift109
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/BlockedStateErrorMapperStub.swift30
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift (renamed from ios/PacketTunnelCoreTests/Mocks/MockDefaultPathObserver.swift)10
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/NetworkCounters.swift4
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift40
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/PingerMock.swift (renamed from ios/PacketTunnelCoreTests/Mocks/MockPinger.swift)6
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift57
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift38
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift19
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/TunnelDeviceInfoStub.swift (renamed from ios/PacketTunnelCoreTests/Mocks/MockTunnelDeviceInfo.swift)6
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/TunnelMonitorStub.swift94
-rw-r--r--ios/PacketTunnelCoreTests/TaskSleepTests.swift28
-rw-r--r--ios/PacketTunnelCoreTests/TunnelMonitorTests.swift14
-rw-r--r--ios/RelaySelector/RelaySelector.swift2
77 files changed, 3766 insertions, 1285 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index d172d39fe3..7a8db6966e 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -47,7 +47,6 @@
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */; };
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4B12940A48700C23744 /* TunnelStore.swift */; };
- 5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */; };
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2C1243203D000F5FF30 /* StringTests.swift */; };
5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
@@ -57,6 +56,10 @@
580810E92A30E17300B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E72A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift */; };
580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */; };
58092E542A8B832E00C3CC72 /* TunnelMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */; };
+ 580D6B8A2AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580D6B892AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift */; };
+ 580D6B8C2AB3369300B2D6E0 /* BlockedStateErrorMapperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580D6B8B2AB3369300B2D6E0 /* BlockedStateErrorMapperProtocol.swift */; };
+ 580D6B8E2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580D6B8D2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift */; };
+ 580D6B922AB360BE00B2D6E0 /* DeviceCheck+BlockedStateReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580D6B912AB360BE00B2D6E0 /* DeviceCheck+BlockedStateReason.swift */; };
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */; };
58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58138E60294871C600684F0C /* DeviceDataThrottling.swift */; };
58153071294CBE8B00D1702E /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; };
@@ -65,14 +68,16 @@
581DA2732A1E227D0046ED47 /* RESTTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2722A1E227D0046ED47 /* RESTTypes.swift */; };
581DA2752A1E283E0046ED47 /* WgKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */; };
581DA2762A1E2FD10046ED47 /* WgKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */; };
- 581F23AD2A8CF92100788AB6 /* MockDefaultPathObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581F23AC2A8CF92100788AB6 /* MockDefaultPathObserver.swift */; };
- 581F23AF2A8CF94D00788AB6 /* MockPinger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581F23AE2A8CF94D00788AB6 /* MockPinger.swift */; };
+ 581F23AD2A8CF92100788AB6 /* DefaultPathObserverFake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581F23AC2A8CF92100788AB6 /* DefaultPathObserverFake.swift */; };
+ 581F23AF2A8CF94D00788AB6 /* PingerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581F23AE2A8CF94D00788AB6 /* PingerMock.swift */; };
5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */; };
5820EDA9288FE064006BF4E4 /* DeviceManagementInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820EDA8288FE064006BF4E4 /* DeviceManagementInteractor.swift */; };
5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820EDAA288FF0D2006BF4E4 /* DeviceRowView.swift */; };
5822C0042A3724A800A3A5FB /* ShadowsocksConfigurationCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9467E8A2A2E0317000DC21F /* ShadowsocksConfigurationCache.swift */; };
5822C0052A3724A800A3A5FB /* ShadowsocksConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9467E872A2DCD57000DC21F /* ShadowsocksConfiguration.swift */; };
5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */; };
+ 582403822A827E1500163DE8 /* RelaySelectorWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */; };
+ 5826B6CB2ABD83E200B1CA13 /* PacketTunnelOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */; };
58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */; };
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB025124117005D0BB5 /* CustomTextField.swift */; };
58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB2251241B3005D0BB5 /* CustomTextView.swift */; };
@@ -80,7 +85,17 @@
582AE3102440A6CA00E6733A /* InputTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */; };
582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1AE229566420055B6EF /* SettingsCell.swift */; };
582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B0229569620055B6EF /* UINavigationBar+Appearance.swift */; };
+ 58342C042AAB61FB003BA12D /* State+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58342C032AAB61FB003BA12D /* State+Extensions.swift */; };
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; };
+ 5838321B2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838321A2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift */; };
+ 5838321D2AC1C54600EA2071 /* TaskSleepTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */; };
+ 5838321F2AC3160A00EA2071 /* Actor+KeyPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838321E2AC3160A00EA2071 /* Actor+KeyPolicy.swift */; };
+ 583832212AC3174700EA2071 /* Actor+NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832202AC3174700EA2071 /* Actor+NetworkReachability.swift */; };
+ 583832232AC3181400EA2071 /* Actor+ErrorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832222AC3181400EA2071 /* Actor+ErrorState.swift */; };
+ 583832252AC318A100EA2071 /* Actor+ConnectionMonitoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832242AC318A100EA2071 /* Actor+ConnectionMonitoring.swift */; };
+ 583832272AC3193600EA2071 /* Actor+SleepCycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832262AC3193600EA2071 /* Actor+SleepCycle.swift */; };
+ 583832292AC3DF1300EA2071 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832282AC3DF1300EA2071 /* Command.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 */; };
583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */; };
@@ -102,6 +117,8 @@
585A02E92A4B283000C6CAFF /* TCPUnsafeListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */; };
585A02EB2A4B285800C6CAFF /* UDPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02EA2A4B285800C6CAFF /* UDPConnection.swift */; };
585A02ED2A4B28F300C6CAFF /* TCPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02EC2A4B28F300C6CAFF /* TCPConnection.swift */; };
+ 585B1FF02AB09F97008AD470 /* VPNConnectionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F360332AAB626300F53531 /* VPNConnectionProtocol.swift */; };
+ 585B1FF22AB0BC69008AD470 /* State+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585B1FF12AB0BC69008AD470 /* State+Extensions.swift */; };
585B4B8726D9098900555C4C /* TunnelStatusNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */; };
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585CA70E25F8C44600B47C62 /* UIMetrics.swift */; };
585E820327F3285E00939F0E /* SendStoreReceiptOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */; };
@@ -113,6 +130,7 @@
5864AF0729C78843005B0CD9 /* SettingsCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */; };
5864AF0829C78849005B0CD9 /* CellFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864AF0129C7879B005B0CD9 /* CellFactoryProtocol.swift */; };
5864AF0929C78850005B0CD9 /* PreferencesCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864AF0229C7879B005B0CD9 /* PreferencesCellFactory.swift */; };
+ 5864AF7D2A9F4DC9008BC928 /* SettingsReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864AF7C2A9F4DC9008BC928 /* SettingsReader.swift */; };
5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867770D29096984006F721F /* OutOfTimeInteractor.swift */; };
58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */; };
58677712290976FB006F721F /* SettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58677711290976FB006F721F /* SettingsInteractor.swift */; };
@@ -127,7 +145,10 @@
586A950C290125EE007BAF2B /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB122488ED2100095626 /* AlertPresenter.swift */; };
586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; };
586A950F29012BEE007BAF2B /* AddressCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114028F841390037AF9A /* AddressCacheTracker.swift */; };
+ 586C14582AC463BB00245C01 /* CommandChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C14572AC463BB00245C01 /* CommandChannelTests.swift */; };
+ 586C145A2AC4735F00245C01 /* Actor+Public.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C14592AC4735F00245C01 /* Actor+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 */; };
5871167F2910035700D41AAC /* PreferencesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871167E2910035700D41AAC /* PreferencesInteractor.swift */; };
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */; };
5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */; };
@@ -158,17 +179,16 @@
587EB66A270EFACB00123C75 /* CharacterSet+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */; };
587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB671271451E300123C75 /* PreferencesViewModel.swift */; };
587EB6742714520600123C75 /* PreferencesDataSourceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */; };
+ 588395602A9DEEA1008B63F6 /* WgAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5883955F2A9DEEA1008B63F6 /* WgAdapter.swift */; };
5883A09E266A5AF7003EFFCB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 587B7543266922BF00DEF7E9 /* Localizable.strings */; };
588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */; };
588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */; };
5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD82227B11080051EB06 /* SelectLocationCell.swift */; };
5888AD87227B17950051EB06 /* SelectLocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD86227B17950051EB06 /* SelectLocationViewController.swift */; };
- 58897EE72ABB337200CC669D /* PacketTunnelErrorWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06410E172934F43B00AFC18C /* PacketTunnelErrorWrapper.swift */; };
588E4EAE28FEEDD8008046E3 /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; };
58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */; };
58907D9524D17B4E00CFC3F5 /* DisconnectSplitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */; };
58915D632A25F8400066445B /* DeviceCheckOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */; };
- 58915D642A25F8B30066445B /* DeviceCheckOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FDF2D82A0BA11900C2B061 /* DeviceCheckOperation.swift */; };
58915D682A25FA080066445B /* DeviceCheckRemoteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58915D672A25FA080066445B /* DeviceCheckRemoteService.swift */; };
58915D692A2601FB0066445B /* WgKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */; };
58915D6E2A26037A0066445B /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58915D6D2A26037A0066445B /* WireGuardKitTypes */; };
@@ -258,22 +278,22 @@
58C7A4572A863FB90060C66F /* TunnelDeviceInfoProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582403162A821FD700163DE8 /* TunnelDeviceInfoProtocol.swift */; };
58C7A4582A863FB90060C66F /* TunnelMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7A42C2A85067A0060C66F /* TunnelMonitorProtocol.swift */; };
58C7A4592A863FB90060C66F /* WgStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3BDAF28A1821A00C8C2C6 /* WgStats.swift */; };
- 58C7A45A2A863FDD0060C66F /* WgAdapterDeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582403142A821FB000163DE8 /* WgAdapterDeviceInfo.swift */; };
58C7A45B2A8640030060C66F /* PacketTunnelPathObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58225D272A84F23B0083D7F1 /* PacketTunnelPathObserver.swift */; };
58C7A45C2A8640490060C66F /* MullvadLogging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223F3294C8FF00029F5F8 /* MullvadLogging.framework */; platformFilter = ios; };
58C7A4692A8643A90060C66F /* IPv4Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 58218E1428B65058000C624F /* IPv4Header.h */; settings = {ATTRIBUTES = (Public, ); }; };
58C7A46A2A8643A90060C66F /* ICMPHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 58218E1628B65396000C624F /* ICMPHeader.h */; settings = {ATTRIBUTES = (Public, ); }; };
58C7A4702A8649ED0060C66F /* PingerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7A46F2A8649ED0060C66F /* PingerTests.swift */; };
+ 58C7AF112ABD8480007EDD7A /* TunnelProviderMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */; };
+ 58C7AF122ABD8480007EDD7A /* TunnelProviderReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2A7290182B000EB5EBA /* TunnelProviderReply.swift */; };
+ 58C7AF132ABD8480007EDD7A /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; };
+ 58C7AF142ABD8480007EDD7A /* RelaySelectorResult+PacketTunnelRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C9B8CC2ABB247700040B46 /* RelaySelectorResult+PacketTunnelRelay.swift */; };
+ 58C7AF152ABD8480007EDD7A /* PacketTunnelRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */; };
+ 58C7AF162ABD84A8007EDD7A /* URLRequestProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D229B6298D1D5200BB5A2D /* URLRequestProxy.swift */; };
+ 58C7AF172ABD84AA007EDD7A /* ProxyURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687AF28EB083800BE7161 /* ProxyURLRequest.swift */; };
+ 58C7AF182ABD84AB007EDD7A /* ProxyURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2AD290185D200EB5EBA /* ProxyURLResponse.swift */; };
58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C8191729FAA2C400DEB1B4 /* NotificationConfiguration.swift */; };
- 58C9B8C32ABB23A500040B46 /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; };
- 58C9B8C42ABB23A500040B46 /* PacketTunnelRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */; };
- 58C9B8C62ABB23E700040B46 /* ProxyURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2AD290185D200EB5EBA /* ProxyURLResponse.swift */; };
- 58C9B8C72ABB23E700040B46 /* TunnelProviderMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */; };
- 58C9B8C82ABB23E700040B46 /* PacketTunnelOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */; };
- 58C9B8C92ABB23E700040B46 /* TunnelProviderReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2A7290182B000EB5EBA /* TunnelProviderReply.swift */; };
- 58C9B8CA2ABB23E700040B46 /* ProxyURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687AF28EB083800BE7161 /* ProxyURLRequest.swift */; };
- 58C9B8CB2ABB23E700040B46 /* URLRequestProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D229B6298D1D5200BB5A2D /* URLRequestProxy.swift */; };
- 58C9B8CD2ABB247700040B46 /* RelaySelectorResult+PacketTunnelRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C9B8CC2ABB247700040B46 /* RelaySelectorResult+PacketTunnelRelay.swift */; };
+ 58C9B8CE2ABB252E00040B46 /* DeviceCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE65922AB1CDE000E53CB5 /* DeviceCheck.swift */; };
+ 58C9B8D02ABB254000040B46 /* DeviceCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE65922AB1CDE000E53CB5 /* DeviceCheck.swift */; };
58C9B8D12ABB255100040B46 /* DeviceCheckOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FDF2D82A0BA11900C2B061 /* DeviceCheckOperation.swift */; };
58C9B8D22ABB255100040B46 /* DeviceStateAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */; };
58C9B8D32ABB255100040B46 /* DeviceCheckRemoteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58915D672A25FA080066445B /* DeviceCheckRemoteService.swift */; };
@@ -350,10 +370,9 @@
58D22422294C921B0029F5F8 /* MullvadLogging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223F3294C8FF00029F5F8 /* MullvadLogging.framework */; };
58D22426294C92750029F5F8 /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; platformFilter = ios; };
58D22435294C975B0029F5F8 /* Operations.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223A5294C8A480029F5F8 /* Operations.framework */; platformFilter = ios; };
+ 58DDA18F2ABC32380039C360 /* Timings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DDA18E2ABC32380039C360 /* Timings.swift */; };
58DF28A52417CB4B00E836B0 /* StorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* StorePaymentManager.swift */; };
- 58E0729D28814AAE008902F8 /* PacketTunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0729C28814AAE008902F8 /* PacketTunnelConfiguration.swift */; };
58E0729F28814ACC008902F8 /* WireGuardLogLevel+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */; };
- 58E072A128814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */; };
58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0A98727C8F46300FE6BDD /* Tunnel.swift */; };
58E0E2842A3718CE002E3420 /* URLSessionShadowsocksTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0E2832A3718CE002E3420 /* URLSessionShadowsocksTransport.swift */; };
58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */; };
@@ -363,9 +382,9 @@
58E511E628DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; };
58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; };
58E7BA192A975DF70068EC3A /* RESTTransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E7BA182A975DF70068EC3A /* RESTTransportProvider.swift */; };
- 58EC067A2A8D208D00BEB973 /* MockTunnelDeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EC06792A8D208D00BEB973 /* MockTunnelDeviceInfo.swift */; };
+ 58E9C3842A4EF15300CFDEAC /* WireGuardAdapter+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E9C3832A4EF15300CFDEAC /* WireGuardAdapter+Async.swift */; };
+ 58EC067A2A8D208D00BEB973 /* TunnelDeviceInfoStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EC06792A8D208D00BEB973 /* TunnelDeviceInfoStub.swift */; };
58EC067C2A8D2A0B00BEB973 /* NetworkCounters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EC067B2A8D2A0B00BEB973 /* NetworkCounters.swift */; };
- 58ED3A142A7C199C0085CE65 /* StartOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ED3A132A7C199C0085CE65 /* StartOptions.swift */; };
58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */; };
58EE2E3B272FF814003BFF93 /* SettingsDataSourceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */; };
58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */; };
@@ -380,6 +399,9 @@
58F2E148276A307400A79513 /* MapConnectionStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */; };
58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */; };
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */; };
+ 58F3F36A2AA08E3C00D3B0A4 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */; };
+ 58F7753D2AB8473200425B47 /* BlockedStateErrorMapperStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F7753C2AB8473200425B47 /* BlockedStateErrorMapperStub.swift */; };
+ 58F775432AB9E3EF00425B47 /* AppMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F775422AB9E3EF00425B47 /* AppMessageHandler.swift */; };
58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */; };
58FB865526E8BF3100F188BC /* StorePaymentManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865426E8BF3100F188BC /* StorePaymentManagerError.swift */; };
58FB865A26EA214400F188BC /* RelayCacheTrackerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865926EA214400F188BC /* RelayCacheTrackerObserver.swift */; };
@@ -394,7 +416,24 @@
58FE25C62AA72779003D1918 /* PacketTunnelCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58C7A4362A863F440060C66F /* PacketTunnelCore.framework */; };
58FE25CB2AA727A9003D1918 /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; };
58FE25CE2AA72802003D1918 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; };
+ 58FE25D42AA729B5003D1918 /* ActorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25D32AA729B5003D1918 /* ActorTests.swift */; };
+ 58FE25D72AA72A8F003D1918 /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5824030C2A811B0000163DE8 /* State.swift */; };
+ 58FE25D82AA72A8F003D1918 /* ConfigurationBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583E60952A9F6D0800DC61EF /* ConfigurationBuilder.swift */; };
+ 58FE25D92AA72A8F003D1918 /* AutoCancellingTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3F3652AA086A400D3B0A4 /* AutoCancellingTask.swift */; };
+ 58FE25DA2AA72A8F003D1918 /* Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E9C3852A4EF1CB00CFDEAC /* Actor.swift */; };
+ 58FE25DB2AA72A8F003D1918 /* StartOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ED3A132A7C199C0085CE65 /* StartOptions.swift */; };
+ 58FE25DC2AA72A8F003D1918 /* AnyTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEBA02A9CA14B00F578F2 /* AnyTask.swift */; };
+ 58FE25DF2AA72A9B003D1918 /* RelaySelectorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */; };
+ 58FE25E12AA72A9B003D1918 /* SettingsReaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E7A2C2A987689006DAB1B /* SettingsReaderProtocol.swift */; };
+ 58FE25E62AA738E8003D1918 /* TunnelAdapterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5819ABC22A8CF02C007B59A6 /* TunnelAdapterProtocol.swift */; };
+ 58FE25EC2AA77639003D1918 /* TunnelMonitorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25EB2AA77638003D1918 /* TunnelMonitorStub.swift */; };
+ 58FE25EE2AA7764E003D1918 /* TunnelAdapterDummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25ED2AA7764E003D1918 /* TunnelAdapterDummy.swift */; };
+ 58FE25F02AA77664003D1918 /* RelaySelectorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */; };
+ 58FE25F22AA77674003D1918 /* SettingsReaderStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25F12AA77674003D1918 /* SettingsReaderStub.swift */; };
+ 58FE25F42AA9D730003D1918 /* Actor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25F32AA9D730003D1918 /* Actor+Extensions.swift */; };
+ 58FE65952AB1D90600E53CB5 /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; };
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; };
+ 58FF23A32AB09BEE003A2AF2 /* DeviceChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF23A22AB09BEE003A2AF2 /* DeviceChecker.swift */; };
7A02D4EB2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 7A02D4EA2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan */; };
7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */; };
7A0C0F632A979C4A0058EFCE /* Coordinator+Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */; };
@@ -479,7 +518,6 @@
A9EC20E82A5D3A8C0040D56E /* CoordinatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */; };
A9EC20F02A5D79ED0040D56E /* TunnelObfuscation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
A9EC20F42A5D96030040D56E /* Midpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EC20F32A5D96030040D56E /* Midpoint.swift */; };
- A9F360342AAB626300F53531 /* VPNConnectionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F360332AAB626300F53531 /* VPNConnectionProtocol.swift */; };
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; };
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; };
E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; };
@@ -796,6 +834,13 @@
remoteGlobalIDString = 58B2FDD22AA71D2A003EB5C6;
remoteInfo = Settings;
};
+ 58FE65972AB1D90600E53CB5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 58CE5E58224146200008646E /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 58D223D4294C8E5E0029F5F8;
+ remoteInfo = MullvadTypes;
+ };
7A88DCD92A8FABBE00D2FF0E /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 58CE5E58224146200008646E /* Project object */;
@@ -949,16 +994,6 @@
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
- 58FE25E52AA72AE9003D1918 /* Embed Frameworks */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 10;
- files = (
- );
- name = "Embed Frameworks";
- runOnlyForDeploymentPostprocessing = 0;
- };
58FE25E92AA7399D003D1918 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -992,7 +1027,6 @@
06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainSettingsStore.swift; sourceTree = "<group>"; };
06410E03292D0F7100AFC18C /* SettingsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsParser.swift; sourceTree = "<group>"; };
06410E06292D108E00AFC18C /* SettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStore.swift; sourceTree = "<group>"; };
- 06410E172934F43B00AFC18C /* PacketTunnelErrorWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelErrorWrapper.swift; sourceTree = "<group>"; };
06799AB428F98CE700ACD94E /* le_root_cert.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = le_root_cert.cer; sourceTree = "<group>"; };
06799ABC28F98E1D00ACD94E /* MullvadREST.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadREST.framework; sourceTree = BUILT_PRODUCTS_DIR; };
06799ABE28F98E1D00ACD94E /* MullvadREST.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadREST.h; sourceTree = "<group>"; };
@@ -1043,6 +1077,10 @@
580909D22876D09A0078138D /* RevokedDeviceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokedDeviceViewController.swift; sourceTree = "<group>"; };
58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTests.swift; sourceTree = "<group>"; };
580CBFB72848D503007878F0 /* OperationConditionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationConditionTests.swift; sourceTree = "<group>"; };
+ 580D6B892AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkPath+NetworkReachability.swift"; sourceTree = "<group>"; };
+ 580D6B8B2AB3369300B2D6E0 /* BlockedStateErrorMapperProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedStateErrorMapperProtocol.swift; sourceTree = "<group>"; };
+ 580D6B8D2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedStateErrorMapper.swift; sourceTree = "<group>"; };
+ 580D6B912AB360BE00B2D6E0 /* DeviceCheck+BlockedStateReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeviceCheck+BlockedStateReason.swift"; sourceTree = "<group>"; };
580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperation.swift; sourceTree = "<group>"; };
580F8B8228197881002E0998 /* TunnelSettingsV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV2.swift; sourceTree = "<group>"; };
580F8B8528197958002E0998 /* DNSSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSSettings.swift; sourceTree = "<group>"; };
@@ -1061,11 +1099,12 @@
581943E228F8010400B0CB5E /* Date+LogFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+LogFormat.swift"; sourceTree = "<group>"; };
581943E328F8010400B0CB5E /* CustomFormatLogHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFormatLogHandler.swift; sourceTree = "<group>"; };
581943E428F8010400B0CB5E /* OSLogHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLogHandler.swift; sourceTree = "<group>"; };
+ 5819ABC22A8CF02C007B59A6 /* TunnelAdapterProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelAdapterProtocol.swift; sourceTree = "<group>"; };
5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAddDNSEntryCell.swift; sourceTree = "<group>"; };
581DA2722A1E227D0046ED47 /* RESTTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTypes.swift; sourceTree = "<group>"; };
581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgKeyRotation.swift; sourceTree = "<group>"; };
- 581F23AC2A8CF92100788AB6 /* MockDefaultPathObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDefaultPathObserver.swift; sourceTree = "<group>"; };
- 581F23AE2A8CF94D00788AB6 /* MockPinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPinger.swift; sourceTree = "<group>"; };
+ 581F23AC2A8CF92100788AB6 /* DefaultPathObserverFake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultPathObserverFake.swift; sourceTree = "<group>"; };
+ 581F23AE2A8CF94D00788AB6 /* PingerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingerMock.swift; sourceTree = "<group>"; };
5820675A26E6576800655B05 /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = "<group>"; };
5820676326E771DB00655B05 /* TunnelManagerErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerErrors.swift; sourceTree = "<group>"; };
5820EDA8288FE064006BF4E4 /* DeviceManagementInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagementInteractor.swift; sourceTree = "<group>"; };
@@ -1075,8 +1114,10 @@
58225D252A84E8A10083D7F1 /* DefaultPathObserverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultPathObserverProtocol.swift; sourceTree = "<group>"; };
58225D272A84F23B0083D7F1 /* PacketTunnelPathObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelPathObserver.swift; sourceTree = "<group>"; };
5823FA5326CE49F600283BF8 /* TunnelObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObserver.swift; sourceTree = "<group>"; };
- 582403142A821FB000163DE8 /* WgAdapterDeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgAdapterDeviceInfo.swift; sourceTree = "<group>"; };
+ 5824030C2A811B0000163DE8 /* State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = "<group>"; };
582403162A821FD700163DE8 /* TunnelDeviceInfoProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDeviceInfoProtocol.swift; sourceTree = "<group>"; };
+ 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorProtocol.swift; sourceTree = "<group>"; };
+ 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapper.swift; sourceTree = "<group>"; };
58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportViewController.swift; sourceTree = "<group>"; };
58293FB025124117005D0BB5 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; };
58293FB2251241B3005D0BB5 /* CustomTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextView.swift; sourceTree = "<group>"; };
@@ -1085,11 +1126,22 @@
582BB1AE229566420055B6EF /* SettingsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = "<group>"; };
582BB1B0229569620055B6EF /* UINavigationBar+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Appearance.swift"; sourceTree = "<group>"; };
582FFA82290A84E700895745 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
+ 58342C032AAB61FB003BA12D /* State+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "State+Extensions.swift"; sourceTree = "<group>"; };
5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = "<group>"; };
5838318A27C40A3900000571 /* Pinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pinger.swift; sourceTree = "<group>"; };
+ 5838321A2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+Mocks.swift"; sourceTree = "<group>"; };
+ 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskSleepTests.swift; sourceTree = "<group>"; };
+ 5838321E2AC3160A00EA2071 /* Actor+KeyPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+KeyPolicy.swift"; sourceTree = "<group>"; };
+ 583832202AC3174700EA2071 /* Actor+NetworkReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+NetworkReachability.swift"; sourceTree = "<group>"; };
+ 583832222AC3181400EA2071 /* Actor+ErrorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+ErrorState.swift"; sourceTree = "<group>"; };
+ 583832242AC318A100EA2071 /* Actor+ConnectionMonitoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+ConnectionMonitoring.swift"; sourceTree = "<group>"; };
+ 583832262AC3193600EA2071 /* Actor+SleepCycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+SleepCycle.swift"; sourceTree = "<group>"; };
+ 583832282AC3DF1300EA2071 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.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>"; };
583E1E292848DF67004838B3 /* OperationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationObserverTests.swift; sourceTree = "<group>"; };
+ 583E60952A9F6D0800DC61EF /* ConfigurationBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationBuilder.swift; sourceTree = "<group>"; };
583FE00B29C0C7FD006E85F9 /* ModalPresentationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalPresentationConfiguration.swift; sourceTree = "<group>"; };
583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSplitViewController.swift; sourceTree = "<group>"; };
583FE01129C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationControllerDismissalInterceptor.swift; sourceTree = "<group>"; };
@@ -1119,6 +1171,7 @@
585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPUnsafeListener.swift; sourceTree = "<group>"; };
585A02EA2A4B285800C6CAFF /* UDPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPConnection.swift; sourceTree = "<group>"; };
585A02EC2A4B28F300C6CAFF /* TCPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPConnection.swift; sourceTree = "<group>"; };
+ 585B1FF12AB0BC69008AD470 /* State+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "State+Extensions.swift"; sourceTree = "<group>"; };
585CA70E25F8C44600B47C62 /* UIMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMetrics.swift; sourceTree = "<group>"; };
585DA87626B024A600B8C587 /* CachedRelays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedRelays.swift; sourceTree = "<group>"; };
585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelProviderMessage.swift; sourceTree = "<group>"; };
@@ -1132,6 +1185,7 @@
5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCellFactory.swift; sourceTree = "<group>"; };
5864AF0129C7879B005B0CD9 /* CellFactoryProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellFactoryProtocol.swift; sourceTree = "<group>"; };
5864AF0229C7879B005B0CD9 /* PreferencesCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesCellFactory.swift; sourceTree = "<group>"; };
+ 5864AF7C2A9F4DC9008BC928 /* SettingsReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsReader.swift; sourceTree = "<group>"; };
5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MullvadVPN.entitlements; sourceTree = "<group>"; };
5867770D29096984006F721F /* OutOfTimeInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutOfTimeInteractor.swift; sourceTree = "<group>"; };
5867770F290975E8006F721F /* SettingsInteractorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInteractorFactory.swift; sourceTree = "<group>"; };
@@ -1143,7 +1197,11 @@
58695A9F2A4ADA9200328DB3 /* TunnelObfuscationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscationTests.swift; sourceTree = "<group>"; };
586A95112901321B007BAF2B /* IPv6Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv6Endpoint.swift; sourceTree = "<group>"; };
586A951329013235007BAF2B /* AnyIPEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPEndpoint.swift; sourceTree = "<group>"; };
+ 586C14572AC463BB00245C01 /* CommandChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandChannelTests.swift; sourceTree = "<group>"; };
+ 586C14592AC4735F00245C01 /* Actor+Public.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+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>"; };
+ 586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Duration.swift"; sourceTree = "<group>"; };
586F2BE129F6916F009E6924 /* shadowsocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = shadowsocks.h; path = "shadowsocks-proxy/include/shadowsocks.h"; sourceTree = "<group>"; };
5871167E2910035700D41AAC /* PreferencesInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesInteractor.swift; sourceTree = "<group>"; };
5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLog.swift; sourceTree = "<group>"; };
@@ -1181,6 +1239,7 @@
587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CharacterSet+IPAddress.swift"; sourceTree = "<group>"; };
587EB671271451E300123C75 /* PreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewModel.swift; sourceTree = "<group>"; };
587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDataSourceDelegate.swift; sourceTree = "<group>"; };
+ 5883955F2A9DEEA1008B63F6 /* WgAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgAdapter.swift; sourceTree = "<group>"; };
588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadTunnelConfigurationOperation.swift; sourceTree = "<group>"; };
588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetAccountOperation.swift; sourceTree = "<group>"; };
5888AD82227B11080051EB06 /* SelectLocationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationCell.swift; sourceTree = "<group>"; };
@@ -1249,6 +1308,7 @@
58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyTransport.swift; sourceTree = "<group>"; };
58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeServerProxy.swift; sourceTree = "<group>"; };
58BDEB9C2A98F69E00F578F2 /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
+ 58BDEBA02A9CA14B00F578F2 /* AnyTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyTask.swift; sourceTree = "<group>"; };
58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTracker.swift; sourceTree = "<group>"; };
58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationConfiguration.swift; sourceTree = "<group>"; };
58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputGroupView.swift; sourceTree = "<group>"; };
@@ -1280,7 +1340,6 @@
58CE5E6A224146210008646E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
58CE5E6F224146210008646E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
58CE5E79224146470008646E /* PacketTunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PacketTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; };
- 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
58CE5E7D224146470008646E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
58CE5E7E224146470008646E /* PacketTunnel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PacketTunnel.entitlements; sourceTree = "<group>"; };
58D0C79323F1CE7000FE9BA7 /* MullvadVPNScreenshots.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNScreenshots.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1294,12 +1353,11 @@
58D223F3294C8FF00029F5F8 /* MullvadLogging.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadLogging.framework; sourceTree = BUILT_PRODUCTS_DIR; };
58D223F5294C8FF00029F5F8 /* MullvadLogging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadLogging.h; sourceTree = "<group>"; };
58D229B6298D1D5200BB5A2D /* URLRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxy.swift; sourceTree = "<group>"; };
+ 58DDA18E2ABC32380039C360 /* Timings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timings.swift; sourceTree = "<group>"; };
58DF28A42417CB4B00E836B0 /* StorePaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentManager.swift; sourceTree = "<group>"; };
58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationSmokeTests.swift; sourceTree = "<group>"; };
58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireGuardAdapterError+Localization.swift"; sourceTree = "<group>"; };
- 58E0729C28814AAE008902F8 /* PacketTunnelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelConfiguration.swift; sourceTree = "<group>"; };
58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireGuardLogLevel+Logging.swift"; sourceTree = "<group>"; };
- 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MullvadEndpoint+WgEndpoint.swift"; sourceTree = "<group>"; };
58E0A98727C8F46300FE6BDD /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = "<group>"; };
58E0E2832A3718CE002E3420 /* URLSessionShadowsocksTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionShadowsocksTransport.swift; sourceTree = "<group>"; };
58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationUIHandler.swift; sourceTree = "<group>"; };
@@ -1311,7 +1369,9 @@
58E511EA28DDE18400B0BCDE /* Error+Chain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Chain.swift"; sourceTree = "<group>"; };
58E7BA182A975DF70068EC3A /* RESTTransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportProvider.swift; sourceTree = "<group>"; };
58E973DD24850EB600096F90 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = "<group>"; };
- 58EC06792A8D208D00BEB973 /* MockTunnelDeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnelDeviceInfo.swift; sourceTree = "<group>"; };
+ 58E9C3832A4EF15300CFDEAC /* WireGuardAdapter+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireGuardAdapter+Async.swift"; sourceTree = "<group>"; };
+ 58E9C3852A4EF1CB00CFDEAC /* Actor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actor.swift; sourceTree = "<group>"; };
+ 58EC06792A8D208D00BEB973 /* TunnelDeviceInfoStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDeviceInfoStub.swift; sourceTree = "<group>"; };
58EC067B2A8D2A0B00BEB973 /* NetworkCounters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkCounters.swift; sourceTree = "<group>"; };
58ECD29123F178FD004298B6 /* Screenshots.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Screenshots.xcconfig; sourceTree = "<group>"; };
58ED3A132A7C199C0085CE65 /* StartOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartOptions.swift; sourceTree = "<group>"; };
@@ -1327,6 +1387,10 @@
58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotateKeyOperation.swift; sourceTree = "<group>"; };
58F3C0A3249CB069003E76BE /* HeaderBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarView.swift; sourceTree = "<group>"; };
58F3C0A524A50155003E76BE /* relays.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = relays.json; sourceTree = "<group>"; };
+ 58F3F3652AA086A400D3B0A4 /* AutoCancellingTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCancellingTask.swift; sourceTree = "<group>"; };
+ 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
+ 58F7753C2AB8473200425B47 /* BlockedStateErrorMapperStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedStateErrorMapperStub.swift; sourceTree = "<group>"; };
+ 58F775422AB9E3EF00425B47 /* AppMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMessageHandler.swift; sourceTree = "<group>"; };
58F7D26427EB50A300E4D821 /* ResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultOperation.swift; sourceTree = "<group>"; };
58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportReviewViewController.swift; sourceTree = "<group>"; };
58FB865426E8BF3100F188BC /* StorePaymentManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentManagerError.swift; sourceTree = "<group>"; };
@@ -1338,7 +1402,15 @@
58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Formatting.swift"; sourceTree = "<group>"; };
58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseButton.swift; sourceTree = "<group>"; };
58FDF2D82A0BA11900C2B061 /* DeviceCheckOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceCheckOperation.swift; sourceTree = "<group>"; };
+ 58FE25D32AA729B5003D1918 /* ActorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActorTests.swift; sourceTree = "<group>"; };
+ 58FE25EB2AA77638003D1918 /* TunnelMonitorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorStub.swift; sourceTree = "<group>"; };
+ 58FE25ED2AA7764E003D1918 /* TunnelAdapterDummy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelAdapterDummy.swift; sourceTree = "<group>"; };
+ 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorStub.swift; sourceTree = "<group>"; };
+ 58FE25F12AA77674003D1918 /* SettingsReaderStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsReaderStub.swift; sourceTree = "<group>"; };
+ 58FE25F32AA9D730003D1918 /* Actor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+Extensions.swift"; sourceTree = "<group>"; };
+ 58FE65922AB1CDE000E53CB5 /* DeviceCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCheck.swift; sourceTree = "<group>"; };
58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticKeyboardResponder.swift; sourceTree = "<group>"; };
+ 58FF23A22AB09BEE003A2AF2 /* DeviceChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceChecker.swift; sourceTree = "<group>"; };
58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
7A02D4EA2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNScreenshots.xctestplan; sourceTree = "<group>"; };
7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = "<group>"; };
@@ -1523,6 +1595,7 @@
A94D691A2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */,
58FE25CB2AA727A9003D1918 /* libRelaySelector.a in Frameworks */,
58C9B8DA2ABB271D00040B46 /* MullvadTransport.framework in Frameworks */,
+ 58FE65952AB1D90600E53CB5 /* MullvadTypes.framework in Frameworks */,
58C7A45C2A8640490060C66F /* MullvadLogging.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -2126,6 +2199,33 @@
path = Protocols;
sourceTree = "<group>";
};
+ 5864AF802A9F52E3008BC928 /* Actor */ = {
+ isa = PBXGroup;
+ children = (
+ 583832282AC3DF1300EA2071 /* Command.swift */,
+ 5838322A2AC3EF9600EA2071 /* CommandChannel.swift */,
+ 58E9C3852A4EF1CB00CFDEAC /* Actor.swift */,
+ 586C14592AC4735F00245C01 /* Actor+Public.swift */,
+ 583832222AC3181400EA2071 /* Actor+ErrorState.swift */,
+ 5838321E2AC3160A00EA2071 /* Actor+KeyPolicy.swift */,
+ 583832262AC3193600EA2071 /* Actor+SleepCycle.swift */,
+ 583832242AC318A100EA2071 /* Actor+ConnectionMonitoring.swift */,
+ 583832202AC3174700EA2071 /* Actor+NetworkReachability.swift */,
+ 58FE25F32AA9D730003D1918 /* Actor+Extensions.swift */,
+ 58DDA18E2ABC32380039C360 /* Timings.swift */,
+ 5824030C2A811B0000163DE8 /* State.swift */,
+ 58342C032AAB61FB003BA12D /* State+Extensions.swift */,
+ 583E60952A9F6D0800DC61EF /* ConfigurationBuilder.swift */,
+ 58BDEBA02A9CA14B00F578F2 /* AnyTask.swift */,
+ 586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */,
+ 58F3F3652AA086A400D3B0A4 /* AutoCancellingTask.swift */,
+ 58ED3A132A7C199C0085CE65 /* StartOptions.swift */,
+ 580D6B892AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift */,
+ 58E7A0312AA0715100C57861 /* Protocols */,
+ );
+ path = Actor;
+ sourceTree = "<group>";
+ };
58695A9E2A4ADA9200328DB3 /* TunnelObfuscationTests */ = {
isa = PBXGroup;
children = (
@@ -2157,9 +2257,21 @@
path = "Notification Providers";
sourceTree = "<group>";
};
+ 588395612A9DF497008B63F6 /* WireGuardAdapter */ = {
+ isa = PBXGroup;
+ children = (
+ 5883955F2A9DEEA1008B63F6 /* WgAdapter.swift */,
+ 58E9C3832A4EF15300CFDEAC /* WireGuardAdapter+Async.swift */,
+ 58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */,
+ 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */,
+ );
+ path = WireGuardAdapter;
+ sourceTree = "<group>";
+ };
58915D662A25F9F20066445B /* DeviceCheck */ = {
isa = PBXGroup;
children = (
+ 58FE65922AB1CDE000E53CB5 /* DeviceCheck.swift */,
58FDF2D82A0BA11900C2B061 /* DeviceCheckOperation.swift */,
58915D672A25FA080066445B /* DeviceCheckRemoteService.swift */,
580810E72A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift */,
@@ -2316,6 +2428,7 @@
58C7A4382A863F450060C66F /* PacketTunnelCore.h */,
58C9B8DF2ABB273700040B46 /* URLRequestProxy */,
58C9B8C52ABB23B400040B46 /* IPC */,
+ 5864AF802A9F52E3008BC928 /* Actor */,
58C7A42E2A85091B0060C66F /* Pinger */,
58E072A228814B96008902F8 /* TunnelMonitor */,
);
@@ -2325,8 +2438,11 @@
58C7A4432A863F490060C66F /* PacketTunnelCoreTests */ = {
isa = PBXGroup;
children = (
+ 58FE25D32AA729B5003D1918 /* ActorTests.swift */,
+ 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */,
58C7A46F2A8649ED0060C66F /* PingerTests.swift */,
58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */,
+ 586C14572AC463BB00245C01 /* CommandChannelTests.swift */,
58EC067D2A8D2B0700BEB973 /* Mocks */,
);
path = PacketTunnelCoreTests;
@@ -2335,7 +2451,6 @@
58C9B8C52ABB23B400040B46 /* IPC */ = {
isa = PBXGroup;
children = (
- 06410E172934F43B00AFC18C /* PacketTunnelErrorWrapper.swift */,
587C575226D2615F005EF767 /* PacketTunnelOptions.swift */,
5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */,
585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */,
@@ -2475,16 +2590,9 @@
children = (
58CE5E7D224146470008646E /* Info.plist */,
58CE5E7E224146470008646E /* PacketTunnel.entitlements */,
- 58E0729C28814AAE008902F8 /* PacketTunnelConfiguration.swift */,
- 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */,
- 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */,
- 58ED3A132A7C199C0085CE65 /* StartOptions.swift */,
- 58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */,
- 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */,
- 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */,
- 582403142A821FB000163DE8 /* WgAdapterDeviceInfo.swift */,
- 58225D272A84F23B0083D7F1 /* PacketTunnelPathObserver.swift */,
+ 58F3F3682AA08E2200D3B0A4 /* PacketTunnelProvider */,
58915D662A25F9F20066445B /* DeviceCheck */,
+ 588395612A9DF497008B63F6 /* WireGuardAdapter */,
);
path = PacketTunnel;
sourceTree = "<group>";
@@ -2570,13 +2678,30 @@
path = TunnelMonitor;
sourceTree = "<group>";
};
+ 58E7A0312AA0715100C57861 /* Protocols */ = {
+ isa = PBXGroup;
+ children = (
+ 5819ABC22A8CF02C007B59A6 /* TunnelAdapterProtocol.swift */,
+ 586E7A2C2A987689006DAB1B /* SettingsReaderProtocol.swift */,
+ 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */,
+ 580D6B8B2AB3369300B2D6E0 /* BlockedStateErrorMapperProtocol.swift */,
+ );
+ path = Protocols;
+ sourceTree = "<group>";
+ };
58EC067D2A8D2B0700BEB973 /* Mocks */ = {
isa = PBXGroup;
children = (
- 581F23AC2A8CF92100788AB6 /* MockDefaultPathObserver.swift */,
- 581F23AE2A8CF94D00788AB6 /* MockPinger.swift */,
- 58EC06792A8D208D00BEB973 /* MockTunnelDeviceInfo.swift */,
+ 581F23AC2A8CF92100788AB6 /* DefaultPathObserverFake.swift */,
+ 581F23AE2A8CF94D00788AB6 /* PingerMock.swift */,
+ 58EC06792A8D208D00BEB973 /* TunnelDeviceInfoStub.swift */,
58EC067B2A8D2A0B00BEB973 /* NetworkCounters.swift */,
+ 58FE25EB2AA77638003D1918 /* TunnelMonitorStub.swift */,
+ 58FE25ED2AA7764E003D1918 /* TunnelAdapterDummy.swift */,
+ 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */,
+ 58FE25F12AA77674003D1918 /* SettingsReaderStub.swift */,
+ 58F7753C2AB8473200425B47 /* BlockedStateErrorMapperStub.swift */,
+ 5838321A2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift */,
);
path = Mocks;
sourceTree = "<group>";
@@ -2602,6 +2727,23 @@
path = Assets;
sourceTree = "<group>";
};
+ 58F3F3682AA08E2200D3B0A4 /* PacketTunnelProvider */ = {
+ isa = PBXGroup;
+ children = (
+ 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */,
+ 58F775422AB9E3EF00425B47 /* AppMessageHandler.swift */,
+ 585B1FF12AB0BC69008AD470 /* State+Extensions.swift */,
+ 580D6B912AB360BE00B2D6E0 /* DeviceCheck+BlockedStateReason.swift */,
+ 5864AF7C2A9F4DC9008BC928 /* SettingsReader.swift */,
+ 580D6B8D2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift */,
+ 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */,
+ 58FF23A22AB09BEE003A2AF2 /* DeviceChecker.swift */,
+ 58225D272A84F23B0083D7F1 /* PacketTunnelPathObserver.swift */,
+ 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */,
+ );
+ path = PacketTunnelProvider;
+ sourceTree = "<group>";
+ };
58FBFBE7291622580020E046 /* MullvadRESTTests */ = {
isa = PBXGroup;
children = (
@@ -3013,13 +3155,13 @@
58C7A4322A863F440060C66F /* Sources */,
58C7A4332A863F440060C66F /* Frameworks */,
58C7A4342A863F440060C66F /* Resources */,
- 58FE25E52AA72AE9003D1918 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
58C7A45F2A8640490060C66F /* PBXTargetDependency */,
58FE25CD2AA727A9003D1918 /* PBXTargetDependency */,
+ 58FE65982AB1D90600E53CB5 /* PBXTargetDependency */,
58C9B8DD2ABB271D00040B46 /* PBXTargetDependency */,
);
name = PacketTunnelCore;
@@ -3288,8 +3430,9 @@
58CE5E58224146200008646E /* Project object */ = {
isa = PBXProject;
attributes = {
+ BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1430;
- LastUpgradeCheck = 1420;
+ LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "Mullvad VPN AB";
TargetAttributes = {
063F02722902B63F001FA09F = {
@@ -3314,6 +3457,9 @@
58B0A29F238EE67E00BC001D = {
CreatedOnToolsVersion = 11.2.1;
};
+ 58B2FDD22AA71D2A003EB5C6 = {
+ CreatedOnToolsVersion = 14.3.1;
+ };
58C7A4352A863F440060C66F = {
CreatedOnToolsVersion = 14.3.1;
};
@@ -3600,7 +3746,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
+ shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint PacketTunnel/**/*.swift\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
584023262A406C01007B27AC /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
@@ -3735,7 +3881,6 @@
58915D692A2601FB0066445B /* WgKeyRotation.swift in Sources */,
580810E62A30E13D00B74552 /* DeviceStateAccessorProtocol.swift in Sources */,
58C3FA662A38549D006A450A /* MockFileCache.swift in Sources */,
- 58915D642A25F8B30066445B /* DeviceCheckOperation.swift in Sources */,
7AF6E5F12A95F4A500F2679D /* DurationTests.swift in Sources */,
A9467E7F2A29DEFE000DC21F /* RelayCacheTests.swift in Sources */,
582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */,
@@ -3746,6 +3891,7 @@
58C3FA682A385C89006A450A /* FileCacheTests.swift in Sources */,
58165EBE2A262CBB00688EAD /* WgKeyRotationTests.swift in Sources */,
5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */,
+ 58C9B8D02ABB254000040B46 /* DeviceCheck.swift in Sources */,
580810E92A30E17300B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */,
F07BF2582A26112D00042943 /* InputTextFormatterTests.swift in Sources */,
A9EC20E82A5D3A8C0040D56E /* CoordinatesTests.swift in Sources */,
@@ -3782,23 +3928,45 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 58C9B8C32ABB23A500040B46 /* PacketTunnelStatus.swift in Sources */,
- 58C9B8C42ABB23A500040B46 /* PacketTunnelRelay.swift in Sources */,
+ 58FE25F42AA9D730003D1918 /* Actor+Extensions.swift in Sources */,
+ 58DDA18F2ABC32380039C360 /* Timings.swift in Sources */,
+ 58FE25DF2AA72A9B003D1918 /* RelaySelectorProtocol.swift in Sources */,
58C7A4522A863FB50060C66F /* Pinger.swift in Sources */,
+ 580D6B8C2AB3369300B2D6E0 /* BlockedStateErrorMapperProtocol.swift in Sources */,
+ 58C7AF172ABD84AA007EDD7A /* ProxyURLRequest.swift in Sources */,
+ 58C7AF142ABD8480007EDD7A /* RelaySelectorResult+PacketTunnelRelay.swift in Sources */,
+ 5838321F2AC3160A00EA2071 /* Actor+KeyPolicy.swift in Sources */,
+ 58C7AF122ABD8480007EDD7A /* TunnelProviderReply.swift in Sources */,
58C7A4572A863FB90060C66F /* TunnelDeviceInfoProtocol.swift in Sources */,
58C7A4562A863FB90060C66F /* DefaultPathObserverProtocol.swift in Sources */,
- 58C9B8C82ABB23E700040B46 /* PacketTunnelOptions.swift in Sources */,
+ 58FE25DA2AA72A8F003D1918 /* Actor.swift in Sources */,
+ 58FE25E62AA738E8003D1918 /* TunnelAdapterProtocol.swift in Sources */,
+ 583832252AC318A100EA2071 /* Actor+ConnectionMonitoring.swift in Sources */,
58C7A4552A863FB90060C66F /* TunnelMonitor.swift in Sources */,
+ 58C7AF182ABD84AB007EDD7A /* ProxyURLResponse.swift in Sources */,
58C7A4512A863FB50060C66F /* PingerProtocol.swift in Sources */,
- 58897EE72ABB337200CC669D /* PacketTunnelErrorWrapper.swift in Sources */,
- 58C9B8CA2ABB23E700040B46 /* ProxyURLRequest.swift in Sources */,
+ 583832292AC3DF1300EA2071 /* Command.swift in Sources */,
+ 583832232AC3181400EA2071 /* Actor+ErrorState.swift in Sources */,
+ 58C7AF112ABD8480007EDD7A /* TunnelProviderMessage.swift in Sources */,
+ 58C7AF162ABD84A8007EDD7A /* URLRequestProxy.swift in Sources */,
+ 58FE25D72AA72A8F003D1918 /* State.swift in Sources */,
+ 58C7AF132ABD8480007EDD7A /* PacketTunnelStatus.swift in Sources */,
58C7A4592A863FB90060C66F /* WgStats.swift in Sources */,
7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */,
- 58C9B8CB2ABB23E700040B46 /* URLRequestProxy.swift in Sources */,
- 58C9B8CD2ABB247700040B46 /* RelaySelectorResult+PacketTunnelRelay.swift in Sources */,
- 58C9B8C92ABB23E700040B46 /* TunnelProviderReply.swift in Sources */,
- 58C9B8C62ABB23E700040B46 /* ProxyURLResponse.swift in Sources */,
- 58C9B8C72ABB23E700040B46 /* TunnelProviderMessage.swift in Sources */,
+ 58FE25DB2AA72A8F003D1918 /* StartOptions.swift in Sources */,
+ 583832212AC3174700EA2071 /* Actor+NetworkReachability.swift in Sources */,
+ 58FE25D82AA72A8F003D1918 /* ConfigurationBuilder.swift in Sources */,
+ 580D6B8A2AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift in Sources */,
+ 5826B6CB2ABD83E200B1CA13 /* PacketTunnelOptions.swift in Sources */,
+ 586E8DB82AAF4AC4007BF3DA /* Task+Duration.swift in Sources */,
+ 5838322B2AC3EF9600EA2071 /* CommandChannel.swift in Sources */,
+ 586C145A2AC4735F00245C01 /* Actor+Public.swift in Sources */,
+ 58342C042AAB61FB003BA12D /* State+Extensions.swift in Sources */,
+ 583832272AC3193600EA2071 /* Actor+SleepCycle.swift in Sources */,
+ 58FE25DC2AA72A8F003D1918 /* AnyTask.swift in Sources */,
+ 58C7AF152ABD8480007EDD7A /* PacketTunnelRelay.swift in Sources */,
+ 58FE25D92AA72A8F003D1918 /* AutoCancellingTask.swift in Sources */,
+ 58FE25E12AA72A9B003D1918 /* SettingsReaderProtocol.swift in Sources */,
58C7A4582A863FB90060C66F /* TunnelMonitorProtocol.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -3807,11 +3975,20 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 58EC067A2A8D208D00BEB973 /* MockTunnelDeviceInfo.swift in Sources */,
+ 58EC067A2A8D208D00BEB973 /* TunnelDeviceInfoStub.swift in Sources */,
+ 586C14582AC463BB00245C01 /* CommandChannelTests.swift in Sources */,
58EC067C2A8D2A0B00BEB973 /* NetworkCounters.swift in Sources */,
- 581F23AD2A8CF92100788AB6 /* MockDefaultPathObserver.swift in Sources */,
+ 58FE25EC2AA77639003D1918 /* TunnelMonitorStub.swift in Sources */,
+ 58FE25EE2AA7764E003D1918 /* TunnelAdapterDummy.swift in Sources */,
+ 581F23AD2A8CF92100788AB6 /* DefaultPathObserverFake.swift in Sources */,
+ 5838321B2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift in Sources */,
+ 5838321D2AC1C54600EA2071 /* TaskSleepTests.swift in Sources */,
58092E542A8B832E00C3CC72 /* TunnelMonitorTests.swift in Sources */,
- 581F23AF2A8CF94D00788AB6 /* MockPinger.swift in Sources */,
+ 58FE25F02AA77664003D1918 /* RelaySelectorStub.swift in Sources */,
+ 581F23AF2A8CF94D00788AB6 /* PingerMock.swift in Sources */,
+ 58FE25F22AA77674003D1918 /* SettingsReaderStub.swift in Sources */,
+ 58F7753D2AB8473200425B47 /* BlockedStateErrorMapperStub.swift in Sources */,
+ 58FE25D42AA729B5003D1918 /* ActorTests.swift in Sources */,
58C7A4702A8649ED0060C66F /* PingerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -3842,7 +4019,6 @@
5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */,
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */,
F0E8E4C92A604E7400ED26A3 /* AccountDeletionInteractor.swift in Sources */,
- A9F360342AAB626300F53531 /* VPNConnectionProtocol.swift in Sources */,
F09A297D2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift in Sources */,
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */,
587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */,
@@ -4033,6 +4209,7 @@
F09A297B2A9F8A9B00EA3B6F /* LogoutDialogueView.swift in Sources */,
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */,
7A21DACF2A30AA3700A787A9 /* UITextField+Appearance.swift in Sources */,
+ 585B1FF02AB09F97008AD470 /* VPNConnectionProtocol.swift in Sources */,
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */,
F09A297C2A9F8A9B00EA3B6F /* VoucherTextField.swift in Sources */,
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */,
@@ -4050,25 +4227,31 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */,
581DA2762A1E2FD10046ED47 /* WgKeyRotation.swift in Sources */,
580810E52A30E13A00B74552 /* DeviceStateAccessorProtocol.swift in Sources */,
- 58C7A45A2A863FDD0060C66F /* WgAdapterDeviceInfo.swift in Sources */,
580810E82A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */,
+ 585B1FF22AB0BC69008AD470 /* State+Extensions.swift in Sources */,
+ 58C9B8CE2ABB252E00040B46 /* DeviceCheck.swift in Sources */,
58915D682A25FA080066445B /* DeviceCheckRemoteService.swift in Sources */,
- 58E0729D28814AAE008902F8 /* PacketTunnelConfiguration.swift in Sources */,
+ 58E9C3842A4EF15300CFDEAC /* WireGuardAdapter+Async.swift in Sources */,
+ 5864AF7D2A9F4DC9008BC928 /* SettingsReader.swift in Sources */,
+ 588395602A9DEEA1008B63F6 /* WgAdapter.swift in Sources */,
58E0729F28814ACC008902F8 /* WireGuardLogLevel+Logging.swift in Sources */,
+ 58FF23A32AB09BEE003A2AF2 /* DeviceChecker.swift in Sources */,
58C76A092A33850E00100D75 /* ApplicationTarget.swift in Sources */,
+ 580D6B922AB360BE00B2D6E0 /* DeviceCheck+BlockedStateReason.swift in Sources */,
583D86482A2678DC0060D63B /* DeviceStateAccessor.swift in Sources */,
+ 58F3F36A2AA08E3C00D3B0A4 /* PacketTunnelProvider.swift in Sources */,
58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */,
- 58E072A128814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift in Sources */,
58C7A45B2A8640030060C66F /* PacketTunnelPathObserver.swift in Sources */,
+ 580D6B8E2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift in Sources */,
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */,
583FE02429C1ACB3006E85F9 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */,
58CE38C728992C8700A6D6E5 /* WireGuardAdapterError+Localization.swift in Sources */,
58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */,
+ 582403822A827E1500163DE8 /* RelaySelectorWrapper.swift in Sources */,
+ 58F775432AB9E3EF00425B47 /* AppMessageHandler.swift in Sources */,
58FDF2D92A0BA11A00C2B061 /* DeviceCheckOperation.swift in Sources */,
- 58ED3A142A7C199C0085CE65 /* StartOptions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -4414,6 +4597,11 @@
target = 58B2FDD22AA71D2A003EB5C6 /* MullvadSettings */;
targetProxy = 58FE25D02AA72802003D1918 /* PBXContainerItemProxy */;
};
+ 58FE65982AB1D90600E53CB5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 58D223D4294C8E5E0029F5F8 /* MullvadTypes */;
+ targetProxy = 58FE65972AB1D90600E53CB5 /* PBXContainerItemProxy */;
+ };
7A88DCDA2A8FABBE00D2FF0E /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 7A88DCCD2A8FABBE00D2FF0E /* Routing */;
diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
index 94ae7ef1dc..12c4e00478 100644
--- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
@@ -950,7 +950,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
guard tunnelManager.deviceState.isLoggedIn else { return false }
switch tunnelManager.tunnelStatus.state {
- case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection):
+ case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .error:
tunnelManager.reconnectTunnel(selectNewRelay: true)
case .disconnecting, .disconnected:
diff --git a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift
index 021afc95ce..87164e0eaa 100644
--- a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift
@@ -55,9 +55,13 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific
// MARK: - Private
private func handleTunnelStatus(_ tunnelStatus: TunnelStatus) {
- let invalidateForTunnelError = updateLastTunnelError(
- tunnelStatus.packetTunnelStatus.lastErrors.first?.localizedDescription
- )
+ let invalidateForTunnelError: Bool
+ if case let .error(blockStateReason) = tunnelStatus.state {
+ invalidateForTunnelError = updateLastTunnelError(blockStateReason.rawValue)
+ } else {
+ invalidateForTunnelError = updateLastTunnelError(nil)
+ }
+
let invalidateForManagerError = updateTunnelManagerError(tunnelStatus.state)
let invalidateForConnectivity = updateConnectivity(tunnelStatus.state)
let invalidateForNetwork = updateNetwork(tunnelStatus.state)
diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
index f7d8346d59..78541be833 100644
--- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
@@ -52,7 +52,9 @@ class MapConnectionStatusOperation: AsyncOperation {
case .reasserting:
fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in
- if packetTunnelStatus.isNetworkReachable {
+ if let blockedStateReason = packetTunnelStatus.blockedStateReason {
+ return .error(blockedStateReason)
+ } else if packetTunnelStatus.isNetworkReachable {
return packetTunnelStatus.tunnelRelay.map { .reconnecting($0) }
} else {
return .waitingForConnectivity(.noConnection)
@@ -62,7 +64,9 @@ class MapConnectionStatusOperation: AsyncOperation {
case .connected:
fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in
- if packetTunnelStatus.isNetworkReachable {
+ if let blockedStateReason = packetTunnelStatus.blockedStateReason {
+ return .error(blockedStateReason)
+ } else if packetTunnelStatus.isNetworkReachable {
return packetTunnelStatus.tunnelRelay.map { .connected($0) }
} else {
return .waitingForConnectivity(.noConnection)
@@ -102,7 +106,9 @@ class MapConnectionStatusOperation: AsyncOperation {
}
fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in
- if packetTunnelStatus.isNetworkReachable {
+ if let blockedStateReason = packetTunnelStatus.blockedStateReason {
+ return .error(blockedStateReason)
+ } else if packetTunnelStatus.isNetworkReachable {
return packetTunnelStatus.tunnelRelay.map { .connecting($0) }
} else {
return .waitingForConnectivity(.noConnection)
diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
index fc52a5872a..7bcce98a73 100644
--- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
@@ -35,7 +35,7 @@ class StopTunnelOperation: ResultOperation<Void> {
finish(result: .success(()))
- case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection):
+ case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .error:
guard let tunnel = interactor.tunnel else {
finish(result: .failure(UnsetTunnelError()))
return
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 4e6bfc6163..9966b40fc7 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -76,7 +76,7 @@ final class TunnelManager: StorePaymentObserver {
private var _tunnelStatus = TunnelStatus()
/// Last processed device check.
- private var lastDeviceCheck: DeviceCheck?
+ private var lastPacketTunnelKeyRotation: Date?
// MARK: - Initialization
@@ -696,17 +696,23 @@ final class TunnelManager: StorePaymentObserver {
_tunnelStatus = newTunnelStatus
- if let deviceCheck = newTunnelStatus.packetTunnelStatus.deviceCheck {
- handleDeviceCheck(deviceCheck)
+ // Packet tunnel may have attempted or rotated the key.
+ // In that case we have to reload device state from Keychain as it's likely was modified by packet tunnel.
+ let newPacketTunnelKeyRotation = newTunnelStatus.packetTunnelStatus.lastKeyRotation
+ if lastPacketTunnelKeyRotation != newPacketTunnelKeyRotation {
+ lastPacketTunnelKeyRotation = newPacketTunnelKeyRotation
+ refreshDeviceState()
}
+ // TODO: handle blocked state (error state). See how handleRestError() manages invalid account or revoked device.
+
switch newTunnelStatus.state {
case .connecting, .reconnecting:
// Start polling tunnel status to keep the relay information up to date
// while the tunnel process is trying to connect.
startPollingTunnelStatus(interval: establishingTunnelStatusPollInterval)
- case .connected, .waitingForConnectivity(.noConnection):
+ case .connected, .waitingForConnectivity(.noConnection), .error:
// Start polling tunnel status to keep connectivity status up to date.
startPollingTunnelStatus(interval: establishedTunnelStatusPollInterval)
@@ -724,53 +730,6 @@ final class TunnelManager: StorePaymentObserver {
return newTunnelStatus
}
- private func handleDeviceCheck(_ deviceCheck: DeviceCheck) {
- // Bail immediately when last device check is identical.
- guard lastDeviceCheck != deviceCheck else { return }
-
- // Packet tunnel may have attempted or rotated the key.
- // In that case we have to reload device state from Keychain as it's likely was modified by packet tunnel.
- if lastDeviceCheck?.keyRotationStatus != deviceCheck.keyRotationStatus {
- switch deviceCheck.keyRotationStatus {
- case .attempted, .succeeded:
- refreshDeviceState()
- case .noAction:
- break
- }
- }
-
- // Packet tunnel detected that device is revoked.
- if lastDeviceCheck?.deviceVerdict != deviceCheck.deviceVerdict, deviceCheck.deviceVerdict == .revoked {
- scheduleDeviceStateUpdate(taskName: "Set device revoked", reconnectTunnel: false) { deviceState in
- deviceState = .revoked
- }
- }
-
- // Packet tunnel received new account expiry.
- if lastDeviceCheck?.accountVerdict != deviceCheck.accountVerdict {
- switch deviceCheck.accountVerdict {
- case let .expired(accountData), let .active(accountData):
- scheduleDeviceStateUpdate(taskName: "Update account expiry", reconnectTunnel: false) { deviceState in
- guard case .loggedIn(var storedAccountData, let storedDeviceData) = deviceState else {
- return
- }
-
- if storedAccountData.identifier == accountData.id {
- storedAccountData.expiry = accountData.expiry
- }
-
- deviceState = .loggedIn(storedAccountData, storedDeviceData)
- }
-
- case .invalid:
- break
- }
- }
-
- // Save last device check.
- lastDeviceCheck = deviceCheck
- }
-
fileprivate func setSettings(_ settings: LatestTunnelSettings, persist: Bool) {
nslock.lock()
defer { nslock.unlock() }
diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift
index 5c60f429c8..aed7fb2cc2 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelState.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift
@@ -65,6 +65,9 @@ enum TunnelState: Equatable, CustomStringConvertible {
/// Waiting for connectivity to come back up.
case waitingForConnectivity(WaitingForConnectionReason)
+ /// Error state.
+ case error(BlockedStateReason)
+
var description: String {
switch self {
case .pendingReconnect:
@@ -85,6 +88,8 @@ enum TunnelState: Equatable, CustomStringConvertible {
return "reconnecting to \(tunnelRelay.hostname)"
case .waitingForConnectivity:
return "waiting for connectivity"
+ case let .error(blockedStateReason):
+ return "error state: \(blockedStateReason)"
}
}
@@ -92,7 +97,7 @@ enum TunnelState: Equatable, CustomStringConvertible {
switch self {
case .reconnecting, .connecting, .connected, .waitingForConnectivity(.noConnection):
return true
- case .pendingReconnect, .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork):
+ case .pendingReconnect, .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork), .error:
return false
}
}
@@ -103,7 +108,7 @@ enum TunnelState: Equatable, CustomStringConvertible {
return relay
case let .connecting(relay):
return relay
- case .disconnecting, .disconnected, .waitingForConnectivity, .pendingReconnect:
+ case .disconnecting, .disconnected, .waitingForConnectivity, .pendingReconnect, .error:
return nil
}
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
index 2c26c6a07c..cf76e00d3e 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
@@ -442,7 +442,7 @@ private extension TunnelState {
case .connected:
return .successColor
- case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork):
+ case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error:
return .dangerColor
}
}
@@ -511,6 +511,10 @@ private extension TunnelState {
value: "No network",
comment: ""
)
+
+ case let .error(blockedStateReason):
+ // TODO: Fix me
+ return ""
}
}
@@ -538,6 +542,10 @@ private extension TunnelState {
value: "Switch location",
comment: ""
)
+
+ case let .error(blockedStateReason):
+ // TODO: Fix me
+ return ""
}
}
@@ -614,6 +622,10 @@ private extension TunnelState {
value: "Reconnecting",
comment: ""
)
+
+ case let .error(blockedStateReason):
+ // TODO: Fix me
+ return ""
}
}
@@ -628,7 +640,7 @@ private extension TunnelState {
.waitingForConnectivity(.noConnection):
return [.selectLocation, .cancel]
- case .connected, .reconnecting:
+ case .connected, .reconnecting, .error:
return [.selectLocation, .disconnect]
}
@@ -641,7 +653,7 @@ private extension TunnelState {
.waitingForConnectivity(.noConnection):
return [.cancel]
- case .connected, .reconnecting:
+ case .connected, .reconnecting, .error:
return [.disconnect]
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
index 5a64d50b2a..10095a0f3a 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
@@ -153,7 +153,7 @@ class TunnelViewController: UIViewController, RootContainment {
mapViewController.removeLocationMarker()
contentView.setAnimatingActivity(true)
- case .waitingForConnectivity:
+ case .waitingForConnectivity, .error:
mapViewController.removeLocationMarker()
contentView.setAnimatingActivity(false)
diff --git a/ios/PacketTunnel/DeviceCheck/DeviceCheck.swift b/ios/PacketTunnel/DeviceCheck/DeviceCheck.swift
new file mode 100644
index 0000000000..e84c07a22e
--- /dev/null
+++ b/ios/PacketTunnel/DeviceCheck/DeviceCheck.swift
@@ -0,0 +1,70 @@
+//
+// DeviceCheck.swift
+// PacketTunnel
+//
+// Created by pronebird on 13/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+
+/// The verdict of an account status check.
+enum AccountVerdict: Equatable {
+ /// Account is no longer valid.
+ case invalid
+
+ /// Account is expired.
+ case expired(Account)
+
+ /// Account exists and has enough time left.
+ case active(Account)
+}
+
+/// The verdict of a device status check.
+enum DeviceVerdict: Equatable {
+ /// Device is revoked.
+ case revoked
+
+ /// Device exists but the public key registered on server does not match any longer.
+ case keyMismatch
+
+ /// Device is in good standing and should work as normal.
+ case active
+}
+
+/// Type describing whether key rotation took place and the outcome of it.
+enum KeyRotationStatus: Equatable {
+ /// No rotation took place yet.
+ case noAction
+
+ /// Rotation attempt took place but without success.
+ case attempted(Date)
+
+ /// Rotation attempt took place and succeeded.
+ case succeeded(Date)
+
+ /// Returns `true` if the status is `.succeeded`.
+ var isSucceeded: Bool {
+ if case .succeeded = self {
+ return true
+ } else {
+ return false
+ }
+ }
+}
+
+/**
+ Struct holding data associated with account and device diagnostics and also device key recovery performed by packet
+ tunnel process.
+ */
+struct DeviceCheck: Equatable {
+ /// The verdict of account status check.
+ var accountVerdict: AccountVerdict
+
+ /// The verdict of device status check.
+ var deviceVerdict: DeviceVerdict
+
+ // The status of the last performed key rotation.
+ var keyRotationStatus: KeyRotationStatus
+}
diff --git a/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift b/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift
index b38b3e958e..ce6913b5cc 100644
--- a/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift
+++ b/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift
@@ -42,7 +42,7 @@ final class DeviceCheckOperation: ResultOperation<DeviceCheck> {
remoteSevice: DeviceCheckRemoteServiceProtocol,
deviceStateAccessor: DeviceStateAccessorProtocol,
rotateImmediatelyOnKeyMismatch: Bool,
- completionHandler: @escaping CompletionHandler
+ completionHandler: CompletionHandler? = nil
) {
self.remoteService = remoteSevice
self.deviceStateAccessor = deviceStateAccessor
diff --git a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift
index da8c11726d..9ad8bcbc75 100644
--- a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift
+++ b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift
@@ -25,7 +25,7 @@ struct DeviceCheckRemoteService: DeviceCheckRemoteServiceProtocol {
accountNumber: String,
completion: @escaping (Result<Account, Error>) -> Void
) -> Cancellable {
- accountsProxy.getAccountData(accountNumber: accountNumber, retryStrategy: .noRetry, completion: completion)
+ accountsProxy.getAccountData(accountNumber: accountNumber).execute(completionHandler: completion)
}
func getDevice(
diff --git a/ios/PacketTunnel/MullvadEndpoint+WgEndpoint.swift b/ios/PacketTunnel/MullvadEndpoint+WgEndpoint.swift
deleted file mode 100644
index 5609958b9e..0000000000
--- a/ios/PacketTunnel/MullvadEndpoint+WgEndpoint.swift
+++ /dev/null
@@ -1,23 +0,0 @@
-//
-// MullvadEndpoint+WgEndpoint.swift
-// PacketTunnel
-//
-// Created by pronebird on 15/07/2022.
-// Copyright © 2022 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import MullvadTypes
-import WireGuardKit
-
-extension MullvadEndpoint {
- var ipv4RelayEndpoint: Endpoint {
- Endpoint(host: .ipv4(ipv4Relay.ip), port: .init(integerLiteral: ipv4Relay.port))
- }
-
- var ipv6RelayEndpoint: Endpoint? {
- guard let ipv6Relay else { return nil }
-
- return Endpoint(host: .ipv6(ipv6Relay.ip), port: .init(integerLiteral: ipv6Relay.port))
- }
-}
diff --git a/ios/PacketTunnel/PacketTunnelConfiguration.swift b/ios/PacketTunnel/PacketTunnelConfiguration.swift
deleted file mode 100644
index ffece55207..0000000000
--- a/ios/PacketTunnel/PacketTunnelConfiguration.swift
+++ /dev/null
@@ -1,74 +0,0 @@
-//
-// PacketTunnelConfiguration.swift
-// PacketTunnel
-//
-// Created by pronebird on 15/07/2022.
-// Copyright © 2022 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import MullvadSettings
-import MullvadTypes
-import protocol Network.IPAddress
-import RelaySelector
-import WireGuardKit
-
-struct PacketTunnelConfiguration {
- var deviceState: DeviceState
- var tunnelSettings: LatestTunnelSettings
- var selectorResult: RelaySelectorResult
-}
-
-extension PacketTunnelConfiguration {
- var wgTunnelConfig: TunnelConfiguration {
- let mullvadEndpoint = selectorResult.endpoint
- var peers = [mullvadEndpoint.ipv4RelayEndpoint]
- if let ipv6RelayEndpoint = mullvadEndpoint.ipv6RelayEndpoint {
- peers.append(ipv6RelayEndpoint)
- }
-
- let peerConfigs = peers.compactMap { endpoint -> PeerConfiguration in
- let pubKey = PublicKey(rawValue: selectorResult.endpoint.publicKey)!
- var peerConfig = PeerConfiguration(publicKey: pubKey)
- peerConfig.endpoint = endpoint
- peerConfig.allowedIPs = [
- IPAddressRange(from: "0.0.0.0/0")!,
- IPAddressRange(from: "::/0")!,
- ]
- return peerConfig
- }
-
- var interfaceConfig: InterfaceConfiguration
-
- switch deviceState {
- case let .loggedIn(_, device):
- interfaceConfig = InterfaceConfiguration(privateKey: device.wgKeyData.privateKey)
- interfaceConfig.addresses = [device.ipv4Address, device.ipv6Address]
- interfaceConfig.dns = dnsServers.map { DNSServer(address: $0) }
-
- case .loggedOut, .revoked:
- interfaceConfig = InterfaceConfiguration(privateKey: PrivateKey())
- }
-
- interfaceConfig.listenPort = 0
-
- return TunnelConfiguration(name: nil, interface: interfaceConfig, peers: peerConfigs)
- }
-
- var dnsServers: [IPAddress] {
- let mullvadEndpoint = selectorResult.endpoint
- let dnsSettings = tunnelSettings.dnsSettings
-
- if dnsSettings.effectiveEnableCustomDNS {
- let dnsServers = dnsSettings.customDNSDomains
- .prefix(DNSSettings.maxAllowedCustomDNSDomains)
- return Array(dnsServers)
- } else {
- if let serverAddress = dnsSettings.blockingOptions.serverAddress {
- return [serverAddress]
- } else {
- return [mullvadEndpoint.ipv4Gateway, mullvadEndpoint.ipv6Gateway]
- }
- }
- }
-}
diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift
deleted file mode 100644
index 910c8b2bef..0000000000
--- a/ios/PacketTunnel/PacketTunnelProvider.swift
+++ /dev/null
@@ -1,786 +0,0 @@
-//
-// PacketTunnelProvider.swift
-// PacketTunnel
-//
-// Created by pronebird on 19/03/2019.
-// Copyright © 2019 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import MullvadLogging
-import MullvadREST
-import MullvadSettings
-import MullvadTransport
-import MullvadTypes
-import Network
-import NetworkExtension
-import Operations
-import PacketTunnelCore
-import RelayCache
-import RelaySelector
-import WireGuardKit
-
-/// Restart interval (in seconds) for the tunnel that failed to start early on.
-private let tunnelStartupFailureRestartInterval: Duration = .seconds(2)
-
-/// Delay before trying to reconnect tunnel after private key rotation.
-private let keyRotationTunnelReconnectionDelay: Duration = .minutes(2)
-
-class PacketTunnelProvider: NEPacketTunnelProvider {
- /// Tunnel provider logger.
- private let providerLogger: Logger
-
- /// WireGuard adapter logger.
- private let tunnelLogger: Logger
-
- /// Internal queue.
- private let dispatchQueue = DispatchQueue(label: "PacketTunnel", qos: .utility)
-
- /// WireGuard adapter.
- private var adapter: WireGuardAdapter!
-
- /// Raised once tunnel establishes connection in the very first time, before calling the system
- /// completion handler passed into `startTunnel`.
- private var isConnected = false
-
- /// Raised once tunnel receives the first call to `stopTunnel()`.
- /// Once this happens all requests to reconnect the tunnel will be ignored.
- private var isStopping = false
-
- /// Flag indicating whether network is reachable.
- private var isNetworkReachable = true
-
- /// Struct holding result of the last device check.
- private var deviceCheck: DeviceCheck?
-
- /// Number of consecutive connection failure attempts.
- private var numberOfFailedAttempts: UInt = 0
-
- /// Last wireguard error.
- private var wgError: WireGuardAdapterError?
-
- /// Last configuration read error.
- private var configurationError: Error?
-
- /// Repeating timer used for restarting the tunnel if it had failed during the startup sequence.
- private var tunnelStartupFailureRecoveryTimer: DispatchSourceTimer?
-
- /// Relay cache.
- private let relayCache: RelayCache
-
- /// Current selector result.
- private var selectorResult: RelaySelectorResult?
-
- /// A system completion handler passed from startTunnel and saved for later use once the
- /// connection is established.
- private var startTunnelCompletionHandler: (() -> Void)?
-
- /// Tunnel monitor.
- private var tunnelMonitor: TunnelMonitor!
-
- /// Request proxy used to perform URLRequests bypassing VPN.
- private let urlRequestProxy: URLRequestProxy
-
- /// Account data request proxy
- private let accountsProxy: REST.AccountsProxy
-
- /// Device data request proxy
- private let devicesProxy: REST.DevicesProxy
-
- /// Last device check task.
- private var checkDeviceStateTask: Cancellable?
-
- /// Last task to reconnect the tunnel.
- private var reconnectTunnelTask: Operation?
-
- /// Internal operation queue.
- private let operationQueue = AsyncOperationQueue()
-
- /// Timer for tunnel reconnection. Used to delay reconnection when a private key has just been
- /// rotated, to account for latency in key propagation to relays.
- private var tunnelReconnectionTimer: DispatchSourceTimer?
-
- /// Current device state for the tunnel.
- private var cachedDeviceState: DeviceState?
-
- /// Whether to use the cached device state.
- private var useCachedDeviceState = false
-
- private let constraintsUpdater = RelayConstraintsUpdater()
-
- /// Returns `PacketTunnelStatus` used for sharing with main bundle process.
- private var packetTunnelStatus: PacketTunnelStatus {
- let errors: [PacketTunnelErrorWrapper?] = [
- wgError.flatMap { PacketTunnelErrorWrapper(error: $0) },
- configurationError.flatMap { PacketTunnelErrorWrapper(error: $0) },
- ]
-
- return PacketTunnelStatus(
- lastErrors: errors.compactMap { $0 },
- isNetworkReachable: isNetworkReachable,
- deviceCheck: deviceCheck,
- tunnelRelay: selectorResult?.packetTunnelRelay,
- numberOfFailedAttempts: numberOfFailedAttempts
- )
- }
-
- override init() {
- var loggerBuilder = LoggerBuilder()
- let pid = ProcessInfo.processInfo.processIdentifier
-
- loggerBuilder.metadata["pid"] = .string("\(pid)")
- loggerBuilder.addFileOutput(fileURL: ApplicationConfiguration.logFileURL(for: .packetTunnel))
-
- #if DEBUG
- loggerBuilder.addOSLogOutput(subsystem: ApplicationTarget.packetTunnel.bundleIdentifier)
- #endif
-
- loggerBuilder.install()
-
- providerLogger = Logger(label: "PacketTunnelProvider")
- tunnelLogger = Logger(label: "WireGuard")
-
- let containerURL = ApplicationConfiguration.containerURL
- let addressCache = REST.AddressCache(canWriteToCache: false, cacheDirectory: containerURL)
- addressCache.loadFromFile()
-
- relayCache = RelayCache(cacheDirectory: containerURL)
-
- let urlSession = REST.makeURLSession()
- let urlSessionTransport = URLSessionTransport(urlSession: urlSession)
- let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL)
- let transportProvider = TransportProvider(
- urlSessionTransport: urlSessionTransport,
- relayCache: relayCache,
- addressCache: addressCache,
- shadowsocksCache: shadowsocksCache,
- constraintsUpdater: constraintsUpdater
- )
-
- let proxyFactory = REST.ProxyFactory.makeProxyFactory(
- transportProvider: transportProvider,
- addressCache: addressCache
- )
-
- urlRequestProxy = URLRequestProxy(
- dispatchQueue: dispatchQueue,
- transportProvider: transportProvider
- )
- accountsProxy = proxyFactory.createAccountsProxy()
- devicesProxy = proxyFactory.createDevicesProxy()
-
- super.init()
-
- adapter = createWireGuardAdapter()
-
- tunnelMonitor = createTunnelMonitor(wireGuardAdapter: adapter)
- tunnelMonitor.onEvent = { [weak self] event in
- self?.handleTunnelMonitorEvent(event)
- }
- }
-
- override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
- dispatchQueue.async {
- // Parse relay selector from tunnel options.
- let parsedOptions = self.parseStartOptions(options ?? [:])
- self.providerLogger.debug("\(parsedOptions.logFormat())")
-
- // Read tunnel configuration.
- let tunnelConfiguration: PacketTunnelConfiguration
- do {
- let initialRelay: NextRelay = parsedOptions.selectorResult.map { .set($0) } ?? .automatic
-
- tunnelConfiguration = try self.makeConfiguration(initialRelay)
- } catch {
- self.providerLogger.error(
- error: error,
- message: "Failed to read tunnel configuration when starting the tunnel."
- )
-
- self.configurationError = error
-
- self.startEmptyTunnel(completionHandler: completionHandler)
- self.beginTunnelStartupFailureRecovery()
- return
- }
-
- // Set tunnel status.
- let selectorResult = tunnelConfiguration.selectorResult
- self.selectorResult = selectorResult
- self.providerLogger.debug("Set tunnel relay to \(selectorResult.relay.hostname).")
- self.logIfDeviceHasSameIP(than: tunnelConfiguration.wgTunnelConfig.interface.addresses)
-
- // Start tunnel.
- self.adapter.start(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in
- self.dispatchQueue.async {
- if let error {
- self.providerLogger.error(
- error: error,
- message: "Failed to start the tunnel."
- )
-
- completionHandler(error)
- } else {
- self.providerLogger.debug("Started the tunnel.")
-
- self.tunnelAdapterDidStart()
-
- self.startTunnelCompletionHandler = { [weak self] in
- self?.isConnected = true
- completionHandler(nil)
- }
-
- self.tunnelMonitor.start(
- probeAddress: tunnelConfiguration.selectorResult.endpoint.ipv4Gateway
- )
- }
- }
- }
- }
- }
-
- private func logIfDeviceHasSameIP(than addresses: [IPAddressRange]) {
- let hasIPv4SameAddress = addresses.compactMap { $0.address as? IPv4Address }
- .contains { $0 == ApplicationConfiguration.sameIPv4 }
- let hasIPv6SameAddress = addresses.compactMap { $0.address as? IPv6Address }
- .contains { $0 == ApplicationConfiguration.sameIPv6 }
-
- let isUsingSameIP = (hasIPv4SameAddress || hasIPv6SameAddress) ? "" : "NOT "
- providerLogger.debug("Same IP is \(isUsingSameIP)being used")
- }
-
- override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
- dispatchQueue.async {
- self.providerLogger.debug("Stop the tunnel: \(reason)")
-
- self.isStopping = true
- self.cancelTunnelReconnectionTimer()
- self.cancelTunnelStartupFailureRecovery()
- self.startTunnelCompletionHandler = nil
-
- // Cancel all operations: reconnection requests, network requests.
- self.operationQueue.cancelAllOperations()
-
- // Stop tunnel monitor after all operations are kicked off the queue.
- self.operationQueue.addBarrierBlock {
- self.tunnelMonitor.stop()
-
- self.adapter.stop { error in
- self.dispatchQueue.async {
- if let error {
- self.providerLogger.error(
- error: error,
- message: "Failed to stop the tunnel gracefully."
- )
- } else {
- self.providerLogger.debug("Stopped the tunnel.")
- }
- completionHandler()
- }
- }
- }
- }
- }
-
- override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
- dispatchQueue.async {
- let message: TunnelProviderMessage
- do {
- message = try TunnelProviderMessage(messageData: messageData)
- } catch {
- self.providerLogger.error(error: error, message: "Failed to decode the app message.")
-
- completionHandler?(nil)
- return
- }
-
- self.providerLogger.trace("Received app message: \(message)")
-
- switch message {
- case let .reconnectTunnel(appSelectorResult):
- self.providerLogger.debug("Reconnecting the tunnel...")
-
- let nextRelay: NextRelay = (appSelectorResult ?? self.selectorResult).map { .set($0) } ?? .automatic
- self.reconnectTunnel(to: nextRelay, shouldStopTunnelMonitor: true)
-
- completionHandler?(nil)
-
- case .getTunnelStatus:
- var response: Data?
- do {
- response = try TunnelProviderReply(self.packetTunnelStatus).encode()
- } catch {
- self.providerLogger.error(
- error: error,
- message: "Failed to encode tunnel status reply."
- )
- }
-
- completionHandler?(response)
-
- case let .sendURLRequest(proxyRequest):
- self.urlRequestProxy.sendRequest(proxyRequest) { response in
- var reply: Data?
- do {
- reply = try TunnelProviderReply(response).encode()
- } catch {
- self.providerLogger.error(
- error: error,
- message: "Failed to encode ProxyURLResponse."
- )
- }
- completionHandler?(reply)
- }
-
- case let .cancelURLRequest(id):
- self.urlRequestProxy.cancelRequest(identifier: id)
- completionHandler?(nil)
-
- case .privateKeyRotation:
- self.startTunnelReconnectionTimer(
- reconnectionDelay: keyRotationTunnelReconnectionDelay
- )
- completionHandler?(nil)
- }
- }
- }
-
- override func sleep(completionHandler: @escaping () -> Void) {
- tunnelMonitor.onSleep()
- completionHandler()
- }
-
- override func wake() {
- tunnelMonitor.onWake()
- }
-
- // MARK: - Private: Tunnel monitoring
-
- private func handleTunnelMonitorEvent(_ event: TunnelMonitorEvent) {
- switch event {
- case .connectionEstablished:
- tunnelConnectionEstablished()
-
- case .connectionLost:
- tunnelConnectionLost()
-
- case let .networkReachabilityChanged(isReachable):
- tunnelReachabilityChanged(isReachable)
- }
- }
-
- private func tunnelConnectionEstablished() {
- dispatchPrecondition(condition: .onQueue(dispatchQueue))
-
- providerLogger.debug("Connection established.")
-
- startTunnelCompletionHandler?()
- startTunnelCompletionHandler = nil
-
- numberOfFailedAttempts = 0
-
- checkDeviceStateTask?.cancel()
- checkDeviceStateTask = nil
-
- setReconnecting(false)
- }
-
- private func tunnelConnectionLost() {
- dispatchPrecondition(condition: .onQueue(dispatchQueue))
-
- let (value, isOverflow) = numberOfFailedAttempts.addingReportingOverflow(1)
- numberOfFailedAttempts = isOverflow ? 0 : value
-
- if numberOfFailedAttempts.isMultiple(of: 2) {
- startDeviceCheck()
- }
-
- providerLogger.debug("Recover connection. Picking next relay...")
-
- reconnectTunnel(to: .automatic, shouldStopTunnelMonitor: false)
- }
-
- private func tunnelReachabilityChanged(_ isNetworkReachable: Bool) {
- dispatchPrecondition(condition: .onQueue(dispatchQueue))
-
- guard self.isNetworkReachable != isNetworkReachable else { return }
-
- self.isNetworkReachable = isNetworkReachable
-
- // Switch tunnel into reconnecting state when offline.
- if !isNetworkReachable {
- setReconnecting(true)
- }
- }
-
- // MARK: - Private
-
- private func createWireGuardAdapter() -> WireGuardAdapter {
- WireGuardAdapter(
- with: self,
- shouldHandleReasserting: false,
- logHandler: { [weak self] logLevel, message in
- self?.dispatchQueue.async {
- self?.tunnelLogger.log(level: logLevel.loggerLevel, "\(message)")
- }
- }
- )
- }
-
- private func createTunnelMonitor(wireGuardAdapter: WireGuardAdapter) -> TunnelMonitor {
- TunnelMonitor(
- eventQueue: dispatchQueue,
- pinger: Pinger(replyQueue: dispatchQueue),
- tunnelDeviceInfo: WgAdapterDeviceInfo(adapter: adapter),
- defaultPathObserver: PacketTunnelPathObserver(packetTunnelProvider: self),
- timings: TunnelMonitorTimings()
- )
- }
-
- private func startTunnelReconnectionTimer(reconnectionDelay: Duration) {
- dispatchPrecondition(condition: .onQueue(dispatchQueue))
-
- providerLogger.debug("Delaying tunnel reconnection by \(reconnectionDelay) seconds...")
- useCachedDeviceState = true
-
- let timer = DispatchSource.makeTimerSource(queue: dispatchQueue)
-
- timer.setEventHandler { [weak self] in
- self?.providerLogger.debug("Reconnecting the tunnel...")
-
- let nextRelay: NextRelay = self?.selectorResult
- .map { .set($0) } ?? .automatic
-
- self?.useCachedDeviceState = false
- self?.reconnectTunnel(to: nextRelay, shouldStopTunnelMonitor: true)
- }
-
- timer.setCancelHandler { [weak self] in
- self?.useCachedDeviceState = false
- }
-
- timer.schedule(wallDeadline: .now() + reconnectionDelay)
- timer.activate()
-
- tunnelReconnectionTimer?.cancel()
- tunnelReconnectionTimer = timer
- }
-
- private func cancelTunnelReconnectionTimer() {
- dispatchPrecondition(condition: .onQueue(dispatchQueue))
-
- tunnelReconnectionTimer?.cancel()
- tunnelReconnectionTimer = nil
- }
-
- private func beginTunnelStartupFailureRecovery() {
- dispatchPrecondition(condition: .onQueue(dispatchQueue))
-
- let timer = DispatchSource.makeTimerSource(queue: dispatchQueue)
- timer.setEventHandler { [weak self] in
- guard let self else { return }
-
- providerLogger.debug("Restart the tunnel that had startup failure.")
- reconnectTunnel(to: .automatic, shouldStopTunnelMonitor: false) { [weak self] error in
- if error == nil {
- self?.cancelTunnelStartupFailureRecovery()
- }
- }
- }
-
- timer.schedule(
- wallDeadline: .now() + tunnelStartupFailureRestartInterval,
- repeating: tunnelStartupFailureRestartInterval.timeInterval
- )
- timer.activate()
-
- tunnelStartupFailureRecoveryTimer?.cancel()
- tunnelStartupFailureRecoveryTimer = timer
- }
-
- private func cancelTunnelStartupFailureRecovery() {
- dispatchPrecondition(condition: .onQueue(dispatchQueue))
-
- tunnelStartupFailureRecoveryTimer?.cancel()
- tunnelStartupFailureRecoveryTimer = nil
- }
-
- /**
- Called once the tunnel was able to read configuration and start WireGuard adapter.
- */
- private func tunnelAdapterDidStart() {
- dispatchPrecondition(condition: .onQueue(dispatchQueue))
-
- startDeviceCheck(shouldImmediatelyRotateKeyOnMismatch: true)
- }
-
- private func startEmptyTunnel(completionHandler: @escaping (Error?) -> Void) {
- dispatchPrecondition(condition: .onQueue(dispatchQueue))
-
- let emptyTunnelConfiguration = TunnelConfiguration(
- name: nil,
- interface: InterfaceConfiguration(privateKey: PrivateKey()),
- peers: []
- )
-
- adapter.start(tunnelConfiguration: emptyTunnelConfiguration) { error in
- self.dispatchQueue.async {
- if let error {
- self.providerLogger.error(
- error: error,
- message: "Failed to start an empty tunnel."
- )
-
- completionHandler(error)
- } else {
- self.providerLogger.debug("Started an empty tunnel.")
-
- self.tunnelAdapterDidStart()
-
- self.startTunnelCompletionHandler = { [weak self] in
- self?.isConnected = true
- completionHandler(nil)
- }
- }
- }
- }
- }
-
- private func setReconnecting(_ reconnecting: Bool) {
- // Raise reasserting flag, but only if tunnel has already moved to connected state once.
- // Otherwise keep the app in connecting state until it manages to establish the very first
- // connection.
- if isConnected {
- reasserting = reconnecting
- }
- }
-
- private func parseStartOptions(_ options: [String: NSObject]) -> StartOptions {
- let tunnelOptions = PacketTunnelOptions(rawOptions: options)
- var parsedOptions = StartOptions(launchSource: tunnelOptions.isOnDemand() ? .onDemand : .app)
-
- do {
- if let selectorResult = try tunnelOptions.getSelectorResult() {
- parsedOptions.launchSource = .app
- parsedOptions.selectorResult = selectorResult
- } else {
- parsedOptions.launchSource = tunnelOptions.isOnDemand() ? .onDemand : .system
- }
- } catch {
- providerLogger.error(error: error, message: "Failed to decode relay selector result passed from the app.")
- }
-
- return parsedOptions
- }
-
- private func makeConfiguration(_ nextRelay: NextRelay) throws -> PacketTunnelConfiguration {
- let tunnelSettings = try SettingsManager.readSettings()
- let selectorResult: RelaySelectorResult
-
- var deviceState: DeviceState
- if let cachedDeviceState, useCachedDeviceState {
- deviceState = cachedDeviceState
- } else {
- deviceState = try SettingsManager.readDeviceState()
- cachedDeviceState = deviceState
- }
-
- switch nextRelay {
- case .automatic:
- selectorResult = try selectRelayEndpoint(
- relayConstraints: tunnelSettings.relayConstraints
- )
- case let .set(aSelectorResult):
- selectorResult = aSelectorResult
- }
-
- constraintsUpdater.onNewConstraints?(tunnelSettings.relayConstraints)
-
- return PacketTunnelConfiguration(
- deviceState: deviceState,
- tunnelSettings: tunnelSettings,
- selectorResult: selectorResult
- )
- }
-
- private func reconnectTunnel(
- to nextRelay: NextRelay,
- shouldStopTunnelMonitor: Bool,
- completionHandler: ((Error?) -> Void)? = nil
- ) {
- dispatchPrecondition(condition: .onQueue(dispatchQueue))
-
- // Ignore all requests to reconnect once tunnel is preparing to stop.
- guard !isStopping else { return }
-
- let blockOperation = AsyncBlockOperation(dispatchQueue: dispatchQueue, block: { finish in
- if shouldStopTunnelMonitor {
- self.tunnelMonitor.stop()
- }
-
- self.reconnectTunnelInner(to: nextRelay) { error in
- completionHandler?(error)
- finish(nil)
- }
- })
-
- if let reconnectTunnelTask {
- blockOperation.addDependency(reconnectTunnelTask)
- }
-
- reconnectTunnelTask?.cancel()
- reconnectTunnelTask = blockOperation
-
- operationQueue.addOperation(blockOperation)
- }
-
- private func reconnectTunnelInner(to nextRelay: NextRelay, completionHandler: ((Error?) -> Void)? = nil) {
- dispatchPrecondition(condition: .onQueue(dispatchQueue))
-
- // Read tunnel configuration.
- let tunnelConfiguration: PacketTunnelConfiguration
- do {
- tunnelConfiguration = try makeConfiguration(nextRelay)
- configurationError = nil
- } catch {
- providerLogger.error(
- error: error,
- message: "Failed to produce new configuration."
- )
-
- configurationError = error
-
- completionHandler?(error)
- return
- }
-
- // Copy old relay.
- let oldSelectorResult = selectorResult
- let newTunnelRelay = tunnelConfiguration.selectorResult.packetTunnelRelay
-
- // Update tunnel status.
- selectorResult = tunnelConfiguration.selectorResult
-
- providerLogger.debug("Set tunnel relay to \(newTunnelRelay.hostname).")
- setReconnecting(true)
-
- adapter.update(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in
- self.dispatchQueue.async {
- if let error {
- self.wgError = error
- self.providerLogger.error(
- error: error,
- message: "Failed to update WireGuard configuration."
- )
-
- // Revert to previously used relay selector as it's very likely that we keep
- // using previous configuration.
- self.selectorResult = oldSelectorResult
- self.providerLogger.debug(
- "Reset tunnel relay to \(oldSelectorResult?.relay.hostname ?? "none")."
- )
- self.setReconnecting(false)
- } else {
- self.tunnelMonitor.start(
- probeAddress: tunnelConfiguration.selectorResult.endpoint.ipv4Gateway
- )
- }
- completionHandler?(error)
- }
- }
- }
-
- /// Load relay cache with potential networking to refresh the cache and pick the relay for the
- /// given relay constraints.
- private func selectRelayEndpoint(relayConstraints: RelayConstraints) throws
- -> RelaySelectorResult {
- let cachedRelayList = try relayCache.read()
-
- return try RelaySelector.evaluate(
- relays: cachedRelayList.relays,
- constraints: relayConstraints,
- numberOfFailedAttempts: packetTunnelStatus.numberOfFailedAttempts
- )
- }
-
- // MARK: - Device check
-
- /**
- Start device diagnostics to determine the reason why the tunnel is not functional.
-
- This involves the following steps:
-
- 1. Fetch account and device data.
- 2. Check account validity and whether it has enough time left.
- 3. Verify that current device is registered with backend and that both device and backend point to the same public
- key.
- 4. Rotate WireGuard key on key mismatch.
- */
- private func startDeviceCheck(shouldImmediatelyRotateKeyOnMismatch: Bool = false) {
- let checkOperation = DeviceCheckOperation(
- dispatchQueue: dispatchQueue,
- remoteSevice: DeviceCheckRemoteService(accountsProxy: accountsProxy, devicesProxy: devicesProxy),
- deviceStateAccessor: DeviceStateAccessor(),
- rotateImmediatelyOnKeyMismatch: shouldImmediatelyRotateKeyOnMismatch
- ) { [self] result in
- guard var newDeviceCheck = result.value else { return }
-
- if newDeviceCheck.accountVerdict == .invalid || newDeviceCheck.deviceVerdict == .revoked {
- // Stop tunnel monitor when device is revoked or account is invalid.
- tunnelMonitor.stop()
- } else if case .succeeded = newDeviceCheck.keyRotationStatus {
- // Tell the tunnel to reconnect using new private key if key was rotated dring device check.
- reconnectTunnel(to: .automatic, shouldStopTunnelMonitor: false)
- }
-
- // Retain the last key rotation status that isn't `.noAction` so that UI could keep track of when rotation
- // attempts take place which should give it a hint when to refresh device state from settings.
- if let deviceCheck, newDeviceCheck.keyRotationStatus == .noAction {
- newDeviceCheck.keyRotationStatus = deviceCheck.keyRotationStatus
- }
-
- deviceCheck = newDeviceCheck
- }
-
- operationQueue.addOperation(checkOperation)
- }
-}
-
-/// Enum describing the next relay to connect to.
-private enum NextRelay {
- /// Connect to pre-selected relay.
- case set(RelaySelectorResult)
-
- /// Determine next relay using relay selector.
- case automatic
-}
-
-extension PacketTunnelErrorWrapper {
- init?(error: Error) {
- switch error {
- case let error as WireGuardAdapterError:
- self = .wireguard(error.localizedDescription)
-
- case is UnsupportedSettingsVersionError:
- self = .configuration(.outdatedSchema)
-
- case let keychainError as KeychainError where keychainError == .interactionNotAllowed:
- self = .configuration(.deviceLocked)
-
- case let error as ReadSettingsVersionError:
- if case KeychainError.interactionNotAllowed = error.underlyingError as? KeychainError {
- self = .configuration(.deviceLocked)
- } else {
- self = .configuration(.readFailure)
- }
-
- case is NoRelaysSatisfyingConstraintsError:
- self = .configuration(.noRelaysSatisfyingConstraints)
-
- default:
- return nil
- }
- }
-
- // swiftlint:disable:next file_length
-}
diff --git a/ios/PacketTunnel/PacketTunnelProvider/AppMessageHandler.swift b/ios/PacketTunnel/PacketTunnelProvider/AppMessageHandler.swift
new file mode 100644
index 0000000000..952fcb4bde
--- /dev/null
+++ b/ios/PacketTunnel/PacketTunnelProvider/AppMessageHandler.swift
@@ -0,0 +1,82 @@
+//
+// AppMessageHandler.swift
+// PacketTunnel
+//
+// Created by pronebird on 19/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadLogging
+import PacketTunnelCore
+
+/**
+ Actor handling packet tunnel IPC (app) messages and patching them through to the right facility.
+ */
+struct AppMessageHandler {
+ private let logger = Logger(label: "AppMessageHandler")
+ private let packetTunnelActor: PacketTunnelActor
+ private let urlRequestProxy: URLRequestProxy
+
+ init(packetTunnelActor: PacketTunnelActor, urlRequestProxy: URLRequestProxy) {
+ self.packetTunnelActor = packetTunnelActor
+ self.urlRequestProxy = urlRequestProxy
+ }
+
+ /**
+ Handle app message received via packet tunnel IPC.
+
+ - Message data is expected to be a serialized `TunnelProviderMessage`.
+ - Reply is expected to be wrapped in `TunnelProviderReply`.
+ - Return `nil` in the event of error or when the call site does not expect any reply.
+
+ Calls to reconnect and notify actor when private key is changed are meant to run in parallel because those tasks are serialized in `TunnelManager` and await
+ the acknowledgment from IPC before starting next operation, hence it's critical to return as soon as possible.
+ (See `TunnelManager.reconnectTunnel()`, `SendTunnelProviderMessageOperation`)
+ */
+ func handleAppMessage(_ messageData: Data) async -> Data? {
+ guard let message = decodeMessage(messageData) else { return nil }
+
+ logger.debug("Received app message: \(message)")
+
+ switch message {
+ case let .sendURLRequest(request):
+ return await encodeReply(urlRequestProxy.sendRequest(request))
+
+ case let .cancelURLRequest(id):
+ urlRequestProxy.cancelRequest(identifier: id)
+ return nil
+
+ case .getTunnelStatus:
+ return await encodeReply(packetTunnelActor.state.packetTunnelStatus)
+
+ case .privateKeyRotation:
+ packetTunnelActor.notifyKeyRotation(date: nil)
+ return nil
+
+ case let .reconnectTunnel(selectorResult):
+ packetTunnelActor.reconnect(to: selectorResult.map { .preSelected($0) } ?? .current)
+ return nil
+ }
+ }
+
+ /// Deserialize `TunnelProviderMessage` or return `nil` on error. Errors are logged but ignored.
+ private func decodeMessage(_ data: Data) -> TunnelProviderMessage? {
+ do {
+ return try TunnelProviderMessage(messageData: data)
+ } catch {
+ logger.error(error: error, message: "Failed to decode the app message.")
+ return nil
+ }
+ }
+
+ /// Encode `TunnelProviderReply` or return `nil` on error. Errors are logged but ignored.
+ private func encodeReply<T: Codable>(_ reply: T) -> Data? {
+ do {
+ return try TunnelProviderReply(reply).encode()
+ } catch {
+ logger.error(error: error, message: "Failed to encode the app message reply.")
+ return nil
+ }
+ }
+}
diff --git a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift
new file mode 100644
index 0000000000..a31c508288
--- /dev/null
+++ b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift
@@ -0,0 +1,65 @@
+//
+// BlockedStateErrorMapper.swift
+// PacketTunnel
+//
+// Created by pronebird on 14/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadREST
+import MullvadSettings
+import MullvadTypes
+import PacketTunnelCore
+import RelaySelector
+import WireGuardKit
+
+/**
+ Struct responsible for mapping errors that may occur in the packet tunnel to the `BlockedStateReason`.
+ */
+struct BlockedStateErrorMapper: BlockedStateErrorMapperProtocol {
+ func mapError(_ error: Error) -> BlockedStateReason {
+ switch error {
+ case let error as ReadDeviceDataError:
+ // Such error is thrown by implementations of `SettingsReaderProtocol`.
+ switch error {
+ case .loggedOut:
+ return .deviceLoggedOut
+ case .revoked:
+ return .deviceRevoked
+ }
+
+ case is UnsupportedSettingsVersionError:
+ // Can be returned after updating the app. The tunnel is usually restarted right after but the main app
+ // needs to be launched to perform settings migration.
+ return .outdatedSchema
+
+ case let keychainError as KeychainError where keychainError == .interactionNotAllowed:
+ // Returned when reading device state from Keychain when it is locked on device boot.
+ return .deviceLocked
+
+ case let error as ReadSettingsVersionError:
+ // Returned when reading tunnel settings from Keychain.
+ // interactionNotAllowed is returned when device is locked on boot, otherwise it must be a generic error
+ // when reading settings from keychain.
+ if case KeychainError.interactionNotAllowed = error.underlyingError as? KeychainError {
+ return .deviceLocked
+ } else {
+ return .readSettings
+ }
+
+ case is NoRelaysSatisfyingConstraintsError:
+ // Returned by relay selector when there are no relays satisfying the given constraint.
+ return .noRelaysSatisfyingConstraints
+
+ case is WireGuardAdapterError:
+ // Any errors that originate from wireguard adapter including failure to set tunnel settings using
+ // packet tunnel provider.
+ return .tunnelAdapter
+
+ default:
+ // Everything else in case we introduce new errors and forget to handle them.
+ return .unknown
+ }
+ }
+}
diff --git a/ios/PacketTunnel/PacketTunnelProvider/DeviceCheck+BlockedStateReason.swift b/ios/PacketTunnel/PacketTunnelProvider/DeviceCheck+BlockedStateReason.swift
new file mode 100644
index 0000000000..f6c2d28c1d
--- /dev/null
+++ b/ios/PacketTunnel/PacketTunnelProvider/DeviceCheck+BlockedStateReason.swift
@@ -0,0 +1,25 @@
+//
+// DeviceCheck+BlockedStateReason.swift
+// PacketTunnel
+//
+// Created by pronebird on 14/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import PacketTunnelCore
+
+extension DeviceCheck {
+ /// Returns blocked state reason inferred from the device check result.
+ var blockedStateReason: BlockedStateReason? {
+ if case .invalid = accountVerdict {
+ return .invalidAccount
+ }
+
+ if case .revoked = deviceVerdict {
+ return .deviceRevoked
+ }
+
+ return nil
+ }
+}
diff --git a/ios/PacketTunnel/PacketTunnelProvider/DeviceChecker.swift b/ios/PacketTunnel/PacketTunnelProvider/DeviceChecker.swift
new file mode 100644
index 0000000000..e4fa756b25
--- /dev/null
+++ b/ios/PacketTunnel/PacketTunnelProvider/DeviceChecker.swift
@@ -0,0 +1,57 @@
+//
+// DeviceChecker.swift
+// PacketTunnel
+//
+// Created by pronebird on 12/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadREST
+import MullvadTypes
+import Operations
+import PacketTunnelCore
+
+final class DeviceChecker {
+ private let dispatchQueue = DispatchQueue(label: "DeviceCheckerQueue")
+ private let operationQueue = AsyncOperationQueue.makeSerial()
+
+ private let accountsProxy: REST.AccountsProxy
+ private let devicesProxy: REST.DevicesProxy
+
+ init(accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy) {
+ self.accountsProxy = accountsProxy
+ self.devicesProxy = devicesProxy
+ }
+
+ /**
+ Start device diagnostics to determine the reason why the tunnel is not functional.
+
+ This involves the following steps:
+
+ 1. Fetch account and device data.
+ 2. Check account validity and whether it has enough time left.
+ 3. Verify that current device is registered with backend and that both device and backend point to the same public
+ key.
+ 4. Rotate WireGuard key on key mismatch.
+ */
+ func start(rotateKeyOnMismatch: Bool) async throws -> DeviceCheck {
+ let checkOperation = DeviceCheckOperation(
+ dispatchQueue: dispatchQueue,
+ remoteSevice: DeviceCheckRemoteService(accountsProxy: accountsProxy, devicesProxy: devicesProxy),
+ deviceStateAccessor: DeviceStateAccessor(),
+ rotateImmediatelyOnKeyMismatch: rotateKeyOnMismatch
+ )
+
+ return try await withTaskCancellationHandler {
+ return try await withCheckedThrowingContinuation { continuation in
+ checkOperation.completionHandler = { result in
+ continuation.resume(with: result)
+ }
+ operationQueue.addOperation(checkOperation)
+ }
+ } onCancel: {
+ checkOperation.cancel()
+ }
+ }
+}
diff --git a/ios/PacketTunnel/NEProviderStopReason+Debug.swift b/ios/PacketTunnel/PacketTunnelProvider/NEProviderStopReason+Debug.swift
index 35d5339c58..35d5339c58 100644
--- a/ios/PacketTunnel/NEProviderStopReason+Debug.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider/NEProviderStopReason+Debug.swift
diff --git a/ios/PacketTunnel/PacketTunnelPathObserver.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelPathObserver.swift
index b16c62f705..b16c62f705 100644
--- a/ios/PacketTunnel/PacketTunnelPathObserver.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelPathObserver.swift
diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
new file mode 100644
index 0000000000..9c4e53c79d
--- /dev/null
+++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
@@ -0,0 +1,266 @@
+//
+// PacketTunnelProvider.swift
+// PacketTunnel
+//
+// Created by pronebird on 31/08/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadLogging
+import MullvadREST
+import MullvadTransport
+import MullvadTypes
+import NetworkExtension
+import PacketTunnelCore
+import RelayCache
+
+class PacketTunnelProvider: NEPacketTunnelProvider {
+ private let internalQueue = DispatchQueue(label: "PacketTunnel-internalQueue")
+ private let providerLogger: Logger
+ private let constraintsUpdater = RelayConstraintsUpdater()
+
+ private var actor: PacketTunnelActor!
+ private var appMessageHandler: AppMessageHandler!
+ private var stateObserverTask: AnyTask?
+ private var deviceChecker: DeviceChecker!
+ private var isLoggedSameIP = false
+
+ override init() {
+ Self.configureLogging()
+
+ providerLogger = Logger(label: "PacketTunnelProvider")
+
+ let containerURL = ApplicationConfiguration.containerURL
+ let addressCache = REST.AddressCache(canWriteToCache: false, cacheDirectory: containerURL)
+ addressCache.loadFromFile()
+
+ let relayCache = RelayCache(cacheDirectory: containerURL)
+
+ let urlSession = REST.makeURLSession()
+ let urlSessionTransport = URLSessionTransport(urlSession: urlSession)
+ let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL)
+ let transportProvider = TransportProvider(
+ urlSessionTransport: urlSessionTransport,
+ relayCache: relayCache,
+ addressCache: addressCache,
+ shadowsocksCache: shadowsocksCache,
+ constraintsUpdater: constraintsUpdater
+ )
+
+ super.init()
+
+ let adapter = WgAdapter(packetTunnelProvider: self)
+
+ let tunnelMonitor = TunnelMonitor(
+ eventQueue: internalQueue,
+ pinger: Pinger(replyQueue: internalQueue),
+ tunnelDeviceInfo: adapter,
+ defaultPathObserver: PacketTunnelPathObserver(packetTunnelProvider: self),
+ timings: TunnelMonitorTimings()
+ )
+
+ let proxyFactory = REST.ProxyFactory.makeProxyFactory(
+ transportProvider: transportProvider,
+ addressCache: addressCache
+ )
+ let accountsProxy = proxyFactory.createAccountsProxy()
+ let devicesProxy = proxyFactory.createDevicesProxy()
+
+ deviceChecker = DeviceChecker(accountsProxy: accountsProxy, devicesProxy: devicesProxy)
+
+ actor = PacketTunnelActor(
+ timings: PacketTunnelActorTimings(),
+ tunnelAdapter: adapter,
+ tunnelMonitor: tunnelMonitor,
+ defaultPathObserver: PacketTunnelPathObserver(packetTunnelProvider: self),
+ blockedStateErrorMapper: BlockedStateErrorMapper(),
+ relaySelector: RelaySelectorWrapper(relayCache: relayCache),
+ settingsReader: SettingsReader()
+ )
+
+ let urlRequestProxy = URLRequestProxy(dispatchQueue: internalQueue, transportProvider: transportProvider)
+
+ appMessageHandler = AppMessageHandler(packetTunnelActor: actor, urlRequestProxy: urlRequestProxy)
+ }
+
+ override func startTunnel(options: [String: NSObject]? = nil) async throws {
+ let startOptions = parseStartOptions(options ?? [:])
+
+ startObservingActorState()
+
+ // Run device check during tunnel startup.
+ // This check is allowed to push new key to server if there are some issues with it.
+ startDeviceCheck(rotateKeyOnMismatch: true)
+
+ actor.start(options: startOptions)
+
+ await actor.waitUntilConnected()
+ }
+
+ override func stopTunnel(with reason: NEProviderStopReason) async {
+ providerLogger.debug("stopTunnel: \(reason)")
+
+ stopObservingActorState()
+
+ actor.stop()
+
+ await actor.waitUntilDisconnected()
+ }
+
+ override func handleAppMessage(_ messageData: Data) async -> Data? {
+ return await appMessageHandler.handleAppMessage(messageData)
+ }
+
+ override func sleep() async {
+ actor.onSleep()
+ }
+
+ override func wake() {
+ actor.onWake()
+ }
+}
+
+extension PacketTunnelProvider {
+ override func setTunnelNetworkSettings(
+ _ tunnelNetworkSettings: NETunnelNetworkSettings?,
+ completionHandler: ((Error?) -> Void)? = nil
+ ) {
+ if let networkSettings = tunnelNetworkSettings as? NEPacketTunnelNetworkSettings {
+ let ipv4Addresses = networkSettings.ipv4Settings?.addresses.compactMap { IPv4Address($0) } ?? []
+ let ipv6Addresses = networkSettings.ipv6Settings?.addresses.compactMap { IPv6Address($0) } ?? []
+ let allIPAddresses: [IPAddress] = ipv4Addresses + ipv6Addresses
+
+ if !allIPAddresses.isEmpty, !isLoggedSameIP {
+ isLoggedSameIP = true
+ logIfDeviceHasSameIP(than: allIPAddresses)
+ }
+ }
+
+ super.setTunnelNetworkSettings(tunnelNetworkSettings, completionHandler: completionHandler)
+ }
+
+ private func logIfDeviceHasSameIP(than addresses: [IPAddress]) {
+ let hasIPv4SameAddress = addresses.compactMap { $0 as? IPv4Address }
+ .contains { $0 == ApplicationConfiguration.sameIPv4 }
+ let hasIPv6SameAddress = addresses.compactMap { $0 as? IPv6Address }
+ .contains { $0 == ApplicationConfiguration.sameIPv6 }
+
+ let isUsingSameIP = (hasIPv4SameAddress || hasIPv6SameAddress) ? "" : "NOT "
+ providerLogger.debug("Same IP is \(isUsingSameIP)being used")
+ }
+}
+
+extension PacketTunnelProvider {
+ private static func configureLogging() {
+ var loggerBuilder = LoggerBuilder()
+ let pid = ProcessInfo.processInfo.processIdentifier
+ loggerBuilder.metadata["pid"] = .string("\(pid)")
+ loggerBuilder.addFileOutput(fileURL: ApplicationConfiguration.logFileURL(for: .packetTunnel))
+ #if DEBUG
+ loggerBuilder.addOSLogOutput(subsystem: ApplicationTarget.packetTunnel.bundleIdentifier)
+ #endif
+ loggerBuilder.install()
+ }
+
+ private func parseStartOptions(_ options: [String: NSObject]) -> StartOptions {
+ let tunnelOptions = PacketTunnelOptions(rawOptions: options)
+ var parsedOptions = StartOptions(launchSource: tunnelOptions.isOnDemand() ? .onDemand : .app)
+
+ do {
+ if let selectorResult = try tunnelOptions.getSelectorResult() {
+ parsedOptions.launchSource = .app
+ parsedOptions.selectorResult = selectorResult
+ } else if !tunnelOptions.isOnDemand() {
+ parsedOptions.launchSource = .system
+ }
+ } catch {
+ providerLogger.error(error: error, message: "Failed to decode relay selector result passed from the app.")
+ }
+
+ return parsedOptions
+ }
+}
+
+// MARK: - State observer
+
+extension PacketTunnelProvider {
+ private func startObservingActorState() {
+ stopObservingActorState()
+
+ stateObserverTask = Task {
+ let stateStream = await self.actor.states
+ var lastConnectionAttempt: UInt = 0
+
+ for await newState in stateStream {
+ // Pass relay constraints retrieved during the last read from setting into transport provider.
+ if let relayConstraints = newState.relayConstraints {
+ constraintsUpdater.onNewConstraints?(relayConstraints)
+ }
+
+ // Tell packet tunnel when reconnection begins.
+ // Packet tunnel moves to `NEVPNStatus.reasserting` state once `reasserting` flag is set to `true`.
+ if case .reconnecting = newState, !self.reasserting {
+ self.reasserting = true
+ }
+
+ // Tell packet tunnel when reconnection ends.
+ // Packet tunnel moves to `NEVPNStatus.connected` state once `reasserting` flag is set to `false`.
+ if case .connected = newState, self.reasserting {
+ self.reasserting = false
+ }
+
+ switch newState {
+ case let .reconnecting(connState), let .connecting(connState):
+ let connectionAttempt = connState.connectionAttemptCount
+
+ // Start device check every second failure attempt to connect.
+ if lastConnectionAttempt != connectionAttempt, connectionAttempt > 0,
+ connectionAttempt.isMultiple(of: 2) {
+ startDeviceCheck()
+ }
+
+ // Cache last connection attempt to filter out repeating calls.
+ lastConnectionAttempt = connectionAttempt
+
+ case .initial, .connected, .disconnecting, .disconnected, .error:
+ break
+ }
+ }
+ }
+ }
+
+ private func stopObservingActorState() {
+ stateObserverTask?.cancel()
+ stateObserverTask = nil
+ }
+}
+
+// MARK: - Device check
+
+extension PacketTunnelProvider {
+ private func startDeviceCheck(rotateKeyOnMismatch: Bool = false) {
+ Task {
+ do {
+ try await startDeviceCheckInner(rotateKeyOnMismatch: rotateKeyOnMismatch)
+ } catch {
+ providerLogger.error(error: error, message: "Failed to perform device check.")
+ }
+ }
+ }
+
+ private func startDeviceCheckInner(rotateKeyOnMismatch: Bool) async throws {
+ let result = try await deviceChecker.start(rotateKeyOnMismatch: rotateKeyOnMismatch)
+
+ if let blockedStateReason = result.blockedStateReason {
+ actor.setErrorState(reason: blockedStateReason)
+ }
+
+ switch result.keyRotationStatus {
+ case let .attempted(date), let .succeeded(date):
+ actor.notifyKeyRotation(date: date)
+ case .noAction:
+ break
+ }
+ }
+}
diff --git a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift
new file mode 100644
index 0000000000..31e1e68f3c
--- /dev/null
+++ b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift
@@ -0,0 +1,28 @@
+//
+// RelaySelectorWrapper.swift
+// PacketTunnel
+//
+// Created by pronebird on 08/08/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import PacketTunnelCore
+import RelayCache
+import RelaySelector
+
+struct RelaySelectorWrapper: RelaySelectorProtocol {
+ let relayCache: RelayCache
+
+ func selectRelay(
+ with constraints: RelayConstraints,
+ connectionAttemptFailureCount: UInt
+ ) throws -> RelaySelectorResult {
+ try RelaySelector.evaluate(
+ relays: relayCache.read().relays,
+ constraints: constraints,
+ numberOfFailedAttempts: connectionAttemptFailureCount
+ )
+ }
+}
diff --git a/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift b/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift
new file mode 100644
index 0000000000..48874253a5
--- /dev/null
+++ b/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift
@@ -0,0 +1,75 @@
+//
+// SettingsReader.swift
+// PacketTunnel
+//
+// Created by pronebird on 30/08/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadSettings
+import PacketTunnelCore
+
+struct SettingsReader: SettingsReaderProtocol {
+ func read() throws -> Settings {
+ let settings = try SettingsManager.readSettings()
+ let deviceState = try SettingsManager.readDeviceState()
+ let deviceData = try deviceState.getDeviceData()
+
+ return Settings(
+ privateKey: deviceData.wgKeyData.privateKey,
+ interfaceAddresses: [deviceData.ipv4Address, deviceData.ipv6Address],
+ relayConstraints: settings.relayConstraints,
+ dnsServers: settings.dnsSettings.selectedDNSServers
+ )
+ }
+}
+
+private extension DeviceState {
+ /**
+ Returns `StoredDeviceState` if device is logged in, otherwise throws an error.
+
+ - Throws: an error of type `ReadDeviceDataError` when device is either revoked or logged out.
+ - Returns: a copy of `StoredDeviceData` stored as associated value in `DeviceState.loggedIn` variant.
+ */
+ func getDeviceData() throws -> StoredDeviceData {
+ switch self {
+ case .revoked:
+ throw ReadDeviceDataError.revoked
+ case .loggedOut:
+ throw ReadDeviceDataError.loggedOut
+ case let .loggedIn(_, deviceData):
+ return deviceData
+ }
+ }
+}
+
+private extension DNSSettings {
+ /**
+ Converts `DNSSettings` to `SelectedDNSServers` structure.
+ */
+ var selectedDNSServers: SelectedDNSServers {
+ if effectiveEnableCustomDNS {
+ let addresses = Array(customDNSDomains.prefix(DNSSettings.maxAllowedCustomDNSDomains))
+ return .custom(addresses)
+ } else if let serverAddress = blockingOptions.serverAddress {
+ return .blocking(serverAddress)
+ } else {
+ return .gateway
+ }
+ }
+}
+
+/// Error returned when device state is either revoked or logged out.
+public enum ReadDeviceDataError: LocalizedError {
+ case loggedOut, revoked
+
+ public var errorDescription: String? {
+ switch self {
+ case .loggedOut:
+ return "Device is logged out."
+ case .revoked:
+ return "Device is revoked."
+ }
+ }
+}
diff --git a/ios/PacketTunnel/PacketTunnelProvider/State+Extensions.swift b/ios/PacketTunnel/PacketTunnelProvider/State+Extensions.swift
new file mode 100644
index 0000000000..99862e1521
--- /dev/null
+++ b/ios/PacketTunnel/PacketTunnelProvider/State+Extensions.swift
@@ -0,0 +1,57 @@
+//
+// State+Extensions.swift
+// PacketTunnel
+//
+// Created by pronebird on 12/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import PacketTunnelCore
+
+extension State {
+ var packetTunnelStatus: PacketTunnelStatus {
+ var status = PacketTunnelStatus()
+
+ switch self {
+ case let .connecting(connState),
+ let .connected(connState),
+ let .reconnecting(connState),
+ let .disconnecting(connState):
+ switch connState.networkReachability {
+ case .reachable:
+ status.isNetworkReachable = true
+ case .unreachable:
+ status.isNetworkReachable = false
+ case .undetermined:
+ // TODO: fix me
+ status.isNetworkReachable = true
+ }
+
+ status.numberOfFailedAttempts = connState.connectionAttemptCount
+ status.tunnelRelay = connState.selectedRelay.packetTunnelRelay
+
+ case .disconnected, .initial:
+ break
+
+ case let .error(blockedState):
+ status.blockedStateReason = blockedState.reason
+ }
+
+ return status
+ }
+
+ var relayConstraints: RelayConstraints? {
+ switch self {
+ case let .connecting(connState), let .connected(connState), let .reconnecting(connState):
+ return connState.relayConstraints
+
+ case let .error(blockedState):
+ return blockedState.relayConstraints
+
+ case .initial, .disconnecting, .disconnected:
+ return nil
+ }
+ }
+}
diff --git a/ios/PacketTunnel/WgAdapterDeviceInfo.swift b/ios/PacketTunnel/WgAdapterDeviceInfo.swift
deleted file mode 100644
index 11fca33b94..0000000000
--- a/ios/PacketTunnel/WgAdapterDeviceInfo.swift
+++ /dev/null
@@ -1,85 +0,0 @@
-//
-// WgAdapterInfoProvider.swift
-// PacketTunnel
-//
-// Created by pronebird on 08/08/2023.
-// Copyright © 2023 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import PacketTunnelCore
-import WireGuardKit
-
-struct WgAdapterDeviceInfo: TunnelDeviceInfoProtocol {
- let adapter: WireGuardAdapter
-
- var interfaceName: String? {
- return adapter.interfaceName
- }
-
- func getStats() throws -> WgStats {
- var result: String?
-
- let dispatchGroup = DispatchGroup()
- dispatchGroup.enter()
- adapter.getRuntimeConfiguration { string in
- result = string
- dispatchGroup.leave()
- }
-
- guard case .success = dispatchGroup.wait(wallTimeout: .now() + .seconds(1))
- else { throw StatsError.timeout }
- guard let result else { throw StatsError.nilValue }
- guard let newStats = WgStats(from: result) else { throw StatsError.parse }
-
- return newStats
- }
-
- enum StatsError: LocalizedError {
- case timeout, nilValue, parse
-
- var errorDescription: String? {
- switch self {
- case .timeout:
- return "adapter.getRuntimeConfiguration timeout."
- case .nilValue:
- return "Received nil string for stats."
- case .parse:
- return "Couldn't parse stats."
- }
- }
- }
-}
-
-private extension WgStats {
- init?(from string: String) {
- var bytesReceived: UInt64?
- var bytesSent: UInt64?
-
- string.enumerateLines { line, stop in
- if bytesReceived == nil, let value = parseValue("rx_bytes=", in: line) {
- bytesReceived = value
- } else if bytesSent == nil, let value = parseValue("tx_bytes=", in: line) {
- bytesSent = value
- }
-
- if bytesReceived != nil, bytesSent != nil {
- stop = true
- }
- }
-
- guard let bytesReceived, let bytesSent else {
- return nil
- }
-
- self.init(bytesReceived: bytesReceived, bytesSent: bytesSent)
- }
-}
-
-@inline(__always) private func parseValue(_ prefixKey: String, in line: String) -> UInt64? {
- guard line.hasPrefix(prefixKey) else { return nil }
-
- let value = line.dropFirst(prefixKey.count)
-
- return UInt64(value)
-}
diff --git a/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift b/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift
new file mode 100644
index 0000000000..e373b0e573
--- /dev/null
+++ b/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift
@@ -0,0 +1,151 @@
+//
+// WgAdapter.swift
+// PacketTunnel
+//
+// Created by pronebird on 29/08/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadLogging
+import MullvadTypes
+import NetworkExtension
+import PacketTunnelCore
+import WireGuardKit
+
+struct WgAdapter: TunnelAdapterProtocol {
+ let adapter: WireGuardAdapter
+
+ init(packetTunnelProvider: NEPacketTunnelProvider) {
+ let logger = Logger(label: "WireGuard")
+
+ adapter = WireGuardAdapter(
+ with: packetTunnelProvider,
+ shouldHandleReasserting: false,
+ logHandler: { logLevel, string in
+ logger.log(level: logLevel.loggerLevel, "\(string)")
+ }
+ )
+ }
+
+ func start(configuration: TunnelAdapterConfiguration) async throws {
+ let wgConfig = configuration.asWgConfig
+ do {
+ try await adapter.update(tunnelConfiguration: wgConfig)
+ } catch WireGuardAdapterError.invalidState {
+ try await adapter.start(tunnelConfiguration: wgConfig)
+ }
+ }
+
+ func stop() async throws {
+ try await adapter.stop()
+ }
+}
+
+extension WgAdapter: TunnelDeviceInfoProtocol {
+ var interfaceName: String? {
+ return adapter.interfaceName
+ }
+
+ func getStats() throws -> WgStats {
+ var result: String?
+
+ let dispatchGroup = DispatchGroup()
+ dispatchGroup.enter()
+ adapter.getRuntimeConfiguration { string in
+ result = string
+ dispatchGroup.leave()
+ }
+
+ guard case .success = dispatchGroup.wait(wallTimeout: .now() + 1) else { throw StatsError.timeout }
+ guard let result else { throw StatsError.nilValue }
+ guard let newStats = WgStats(from: result) else { throw StatsError.parse }
+
+ return newStats
+ }
+
+ enum StatsError: LocalizedError {
+ case timeout, nilValue, parse
+
+ var errorDescription: String? {
+ switch self {
+ case .timeout:
+ return "adapter.getRuntimeConfiguration() timeout."
+ case .nilValue:
+ return "Received nil string for stats."
+ case .parse:
+ return "Couldn't parse stats."
+ }
+ }
+ }
+}
+
+private extension TunnelAdapterConfiguration {
+ var asWgConfig: TunnelConfiguration {
+ var interfaceConfig = InterfaceConfiguration(privateKey: privateKey)
+ interfaceConfig.addresses = interfaceAddresses
+ interfaceConfig.dns = dns.map { DNSServer(address: $0) }
+ interfaceConfig.listenPort = 0
+
+ var peers: [PeerConfiguration] = []
+ if let peer {
+ var peerConfig = PeerConfiguration(publicKey: peer.publicKey)
+ peerConfig.endpoint = peer.endpoint.wgEndpoint
+ peerConfig.allowedIPs = [
+ IPAddressRange(from: "0.0.0.0/0")!,
+ IPAddressRange(from: "::/0")!,
+ ]
+ peers.append(peerConfig)
+ }
+
+ return TunnelConfiguration(
+ name: nil,
+ interface: interfaceConfig,
+ peers: peers
+ )
+ }
+}
+
+private extension AnyIPEndpoint {
+ var wgEndpoint: Endpoint {
+ switch self {
+ case let .ipv4(endpoint):
+ return Endpoint(host: .ipv4(endpoint.ip), port: .init(integerLiteral: endpoint.port))
+ case let .ipv6(endpoint):
+ return Endpoint(host: .ipv6(endpoint.ip), port: .init(integerLiteral: endpoint.port))
+ }
+ }
+}
+
+private extension WgStats {
+ init?(from string: String) {
+ var bytesReceived: UInt64?
+ var bytesSent: UInt64?
+
+ string.enumerateLines { line, stop in
+ if bytesReceived == nil, let value = parseValue("rx_bytes=", in: line) {
+ bytesReceived = value
+ } else if bytesSent == nil, let value = parseValue("tx_bytes=", in: line) {
+ bytesSent = value
+ }
+
+ if bytesReceived != nil, bytesSent != nil {
+ stop = true
+ }
+ }
+
+ guard let bytesReceived, let bytesSent else {
+ return nil
+ }
+
+ self.init(bytesReceived: bytesReceived, bytesSent: bytesSent)
+ }
+}
+
+@inline(__always) private func parseValue(_ prefixKey: String, in line: String) -> UInt64? {
+ guard line.hasPrefix(prefixKey) else { return nil }
+
+ let value = line.dropFirst(prefixKey.count)
+
+ return UInt64(value)
+}
diff --git a/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapter+Async.swift b/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapter+Async.swift
new file mode 100644
index 0000000000..1644597b94
--- /dev/null
+++ b/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapter+Async.swift
@@ -0,0 +1,48 @@
+//
+// WireGuardAdapter+Async.swift
+// PacketTunnel
+//
+// Created by pronebird on 30/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import WireGuardKit
+
+extension WireGuardAdapter {
+ func start(tunnelConfiguration: TunnelConfiguration) async throws {
+ return try await withCheckedThrowingContinuation { continuation in
+ start(tunnelConfiguration: tunnelConfiguration) { error in
+ if let error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume(returning: ())
+ }
+ }
+ }
+ }
+
+ func stop() async throws {
+ return try await withCheckedThrowingContinuation { continuation in
+ stop { error in
+ if let error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume(returning: ())
+ }
+ }
+ }
+ }
+
+ func update(tunnelConfiguration: TunnelConfiguration) async throws {
+ return try await withCheckedThrowingContinuation { continuation in
+ update(tunnelConfiguration: tunnelConfiguration) { error in
+ if let error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume(returning: ())
+ }
+ }
+ }
+ }
+}
diff --git a/ios/PacketTunnel/WireGuardAdapterError+Localization.swift b/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapterError+Localization.swift
index 6a88af373d..6a88af373d 100644
--- a/ios/PacketTunnel/WireGuardAdapterError+Localization.swift
+++ b/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapterError+Localization.swift
diff --git a/ios/PacketTunnel/WireGuardLogLevel+Logging.swift b/ios/PacketTunnel/WireGuardAdapter/WireGuardLogLevel+Logging.swift
index c2bdd051ab..c2bdd051ab 100644
--- a/ios/PacketTunnel/WireGuardLogLevel+Logging.swift
+++ b/ios/PacketTunnel/WireGuardAdapter/WireGuardLogLevel+Logging.swift
diff --git a/ios/PacketTunnelCore/Actor/Actor+ConnectionMonitoring.swift b/ios/PacketTunnelCore/Actor/Actor+ConnectionMonitoring.swift
new file mode 100644
index 0000000000..2980d4d11d
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Actor+ConnectionMonitoring.swift
@@ -0,0 +1,74 @@
+//
+// Actor+ConnectionMonitoring.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 26/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension PacketTunnelActor {
+ /// Assign a closure receiving tunnel monitor events.
+ 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))
+ }
+ }
+
+ /**
+ Handle tunnel monitor event.
+
+ Invoked by comand consumer.
+
+ - Important: this method will suspend and must only be invoked as a part of channel consumer to guarantee transactional execution.
+ */
+ func handleMonitorEvent(_ event: TunnelMonitorEvent) async {
+ switch event {
+ case .connectionEstablished:
+ onEstablishConnection()
+
+ case .connectionLost:
+ await onHandleConnectionRecovery()
+ }
+ }
+
+ /// Reset connection attempt counter and update actor state to `connected` state once connection is established.
+ private func onEstablishConnection() {
+ 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:
+ break
+ }
+ }
+
+ /// Increment connection attempt counter and reconnect the tunnel.
+ private func onHandleConnectionRecovery() async {
+ switch state {
+ case var .connecting(connState):
+ connState.incrementAttemptCount()
+ state = .connecting(connState)
+
+ case var .reconnecting(connState):
+ connState.incrementAttemptCount()
+ state = .reconnecting(connState)
+
+ case var .connected(connState):
+ connState.incrementAttemptCount()
+ state = .connected(connState)
+
+ case .initial, .disconnected, .disconnecting, .error:
+ // Explicit return to prevent reconnecting the tunnel.
+ return
+ }
+
+ // Tunnel monitor should already be paused at this point so don't stop it to avoid a reset of its internal
+ // counters.
+ commandChannel.send(.reconnect(.random, stopTunnelMonitor: false))
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/Actor+ErrorState.swift b/ios/PacketTunnelCore/Actor/Actor+ErrorState.swift
new file mode 100644
index 0000000000..b95e0c8d5e
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Actor+ErrorState.swift
@@ -0,0 +1,148 @@
+//
+// Actor+ErrorState.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 26/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import class WireGuardKitTypes.PrivateKey
+
+extension PacketTunnelActor {
+ /**
+ Transition actor to error state.
+
+ Evaluates the error and maps it to `BlockedStateReason` before switching actor to `.error` state.
+
+ - Important: this method will suspend and must only be invoked as a part of channel consumer to guarantee transactional execution.
+
+ - Parameter error: an error that occurred while starting the tunnel.
+ */
+ func setErrorStateInternal(with error: Error) async {
+ let reason = blockedStateErrorMapper.mapError(error)
+
+ await setErrorStateInternal(with: reason)
+ }
+
+ /**
+ Transition actor to error state.
+
+ Normally actor enters error state on its own, due to unrecoverable errors. However error state can also be induced externally for example in response to
+ device check indicating certain issues that actor is not able to detect on its own such as invalid account or device being revoked on backend.
+
+ - Important: this method will suspend and must only be invoked as a part of channel consumer to guarantee transactional execution.
+
+ - Parameter reason: reason why the actor is entering error state.
+ */
+ func setErrorStateInternal(with reason: BlockedStateReason) async {
+ // Tunnel monitor shouldn't run when in error state.
+ tunnelMonitor.stop()
+
+ if let blockedState = makeBlockedState(reason: reason) {
+ state = .error(blockedState)
+ await configureAdapterForErrorState()
+ }
+ }
+
+ /**
+ Derive `BlockedState` from current `state` updating it with the given block reason.
+
+ - Parameter reason: block reason
+ - Returns: New blocked state that should be assigned to error state, otherwise `nil` when actor is past or at `disconnecting` phase or
+ when actor is already in the error state and no changes need to be made.
+ */
+ private func makeBlockedState(reason: BlockedStateReason) -> BlockedState? {
+ switch state {
+ case .initial:
+ return BlockedState(
+ reason: reason,
+ relayConstraints: nil,
+ currentKey: nil,
+ keyPolicy: .useCurrent,
+ networkReachability: defaultPathObserver.defaultPath?.networkReachability ?? .undetermined,
+ recoveryTask: startRecoveryTaskIfNeeded(reason: reason),
+ priorState: .initial
+ )
+
+ case let .connected(connState):
+ return mapConnectionState(connState, reason: reason, priorState: .connected)
+
+ case let .connecting(connState):
+ return mapConnectionState(connState, reason: reason, priorState: .connecting)
+
+ case let .reconnecting(connState):
+ return mapConnectionState(connState, reason: reason, priorState: .reconnecting)
+
+ case var .error(blockedState):
+ if blockedState.reason != reason {
+ blockedState.reason = reason
+ return blockedState
+ } else {
+ return nil
+ }
+
+ case .disconnecting, .disconnected:
+ return nil
+ }
+ }
+
+ /**
+ Map connection state to blocked state.
+ */
+ private func mapConnectionState(
+ _ connState: ConnectionState,
+ reason: BlockedStateReason,
+ priorState: StatePriorToBlockedState
+ ) -> BlockedState {
+ BlockedState(
+ reason: reason,
+ relayConstraints: connState.relayConstraints,
+ currentKey: connState.currentKey,
+ keyPolicy: connState.keyPolicy,
+ networkReachability: connState.networkReachability,
+ priorState: priorState
+ )
+ }
+
+ /**
+ Configure tunnel with empty WireGuard configuration that consumes all traffic on device emulating a firewall blocking all traffic.
+ */
+ private func configureAdapterForErrorState() async {
+ do {
+ let configurationBuilder = ConfigurationBuilder(
+ privateKey: PrivateKey(),
+ interfaceAddresses: []
+ )
+ try await tunnelAdapter.start(configuration: configurationBuilder.makeConfiguration())
+ } catch {
+ logger.error(error: error, message: "Unable to configure the tunnel for error state.")
+ }
+ }
+
+ /**
+ Start a task that will attempt to reconnect the tunnel periodically, but only if the tunnel can recover from error state automatically.
+
+ See `BlockedStateReason.shouldRestartAutomatically` for more info.
+
+ - Parameter reason: the reason why actor is entering blocked state.
+ - Returns: a task that will attempt to perform periodic recovery when applicable, otherwise `nil`.
+ */
+ private func startRecoveryTaskIfNeeded(reason: BlockedStateReason) -> AutoCancellingTask? {
+ guard reason.shouldRestartAutomatically else { return nil }
+
+ // Use detached task to prevent inheriting current context.
+ let task = Task.detached { [weak self] in
+ while !Task.isCancelled {
+ guard let self else { return }
+
+ try await Task.sleepUsingContinuousClock(for: timings.bootRecoveryPeriodicity)
+
+ // Schedule task to reconnect.
+ commandChannel.send(.reconnect(.random))
+ }
+ }
+
+ return AutoCancellingTask(task)
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/Actor+Extensions.swift b/ios/PacketTunnelCore/Actor/Actor+Extensions.swift
new file mode 100644
index 0000000000..71bd13c057
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Actor+Extensions.swift
@@ -0,0 +1,54 @@
+//
+// Actor+.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 07/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension PacketTunnelActor {
+ /// Returns a stream yielding new value when `state` changes.
+ /// The stream starts with current `state` and ends upon moving to `.disconnected` state.
+ public var states: AsyncStream<State> {
+ AsyncStream { continuation in
+ let cancellable = self.$state.sink { newState in
+ continuation.yield(newState)
+
+ // Finish stream once entered `.disconnected` state.
+ if case .disconnected = newState {
+ continuation.finish()
+ }
+ }
+
+ continuation.onTermination = { _ in
+ cancellable.cancel()
+ }
+ }
+ }
+
+ /// Wait until the `state` moved to `.connected`.
+ /// Should return if the state is `.disconnected` as this is the final state of actor.
+ public func waitUntilConnected() async {
+ for await newState in states {
+ switch newState {
+ case .connected, .disconnected:
+ // Return once either desired or final state is reached.
+ return
+
+ case .connecting, .disconnecting, .error, .initial, .reconnecting:
+ break
+ }
+ }
+ }
+
+ /// Wait until the `state` moved to `.disiconnected`.
+ public func waitUntilDisconnected() async {
+ for await newState in states {
+ if case .disconnected = newState {
+ return
+ }
+ }
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift b/ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift
new file mode 100644
index 0000000000..0cf8351523
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift
@@ -0,0 +1,175 @@
+//
+// Actor+KeyPolicy.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 26/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension PacketTunnelActor {
+ /**
+ Cache WG active key for a period of time, before switching to using the new one stored in settings.
+
+ This function replaces the key policy to `.usePrior` caching the currently used key in associated value.
+
+ That cached key is used by actor for some time until the new key is propagated across relays. Then it flips the key policy back to `.useCurrent` and
+ reconnects the tunnel using new key.
+
+ The `lastKeyRotation` passed as an argument is a simple marker value passed back to UI process with actor state. This date can be used to determine when
+ the main app has to re-read device state from Keychain, since there is no other mechanism to notify other process when packet tunnel mutates keychain store.
+
+ - Parameter lastKeyRotation: date when last key rotation took place.
+ */
+ func cacheActiveKey(lastKeyRotation: Date?) {
+ func mutateConnectionState(_ connState: inout ConnectionState) -> Bool {
+ switch connState.keyPolicy {
+ case .useCurrent:
+ if let currentKey = connState.currentKey {
+ connState.lastKeyRotation = lastKeyRotation
+
+ // Move currentKey into keyPolicy.
+ connState.keyPolicy = .usePrior(currentKey, startKeySwitchTask())
+ connState.currentKey = nil
+
+ return true
+ } else {
+ return false
+ }
+
+ case .usePrior:
+ // It's unlikely that we'll see subsequent key rotations happen frequently.
+ return false
+ }
+ }
+
+ switch state {
+ case var .connecting(connState):
+ if mutateConnectionState(&connState) {
+ state = .connecting(connState)
+ }
+
+ case var .connected(connState):
+ if mutateConnectionState(&connState) {
+ state = .connected(connState)
+ }
+
+ case var .reconnecting(connState):
+ if mutateConnectionState(&connState) {
+ state = .reconnecting(connState)
+ }
+
+ case var .error(blockedState):
+ switch blockedState.keyPolicy {
+ case .useCurrent:
+ // Key policy is preserved between states and key rotation may still happen while in blocked state.
+ // Therefore perform the key switch as normal with one exception that it shouldn't reconnect the tunnel
+ // automatically.
+ if let currentKey = blockedState.currentKey {
+ blockedState.lastKeyRotation = lastKeyRotation
+
+ // Move currentKey into keyPolicy.
+ blockedState.keyPolicy = .usePrior(currentKey, startKeySwitchTask())
+ blockedState.currentKey = nil
+
+ state = .error(blockedState)
+ }
+
+ case .usePrior:
+ break
+ }
+
+ case .initial, .disconnected, .disconnecting:
+ break
+ }
+ }
+
+ /**
+ Switch key policy from `.usePrior` to `.useCurrent` policy and reconnect the tunnel.
+
+ Next reconnection attempt will read the new key from settings.
+ */
+ func switchToCurrentKey() {
+ if switchToCurrentKeyInner() {
+ commandChannel.send(.reconnect(.random))
+ }
+ }
+
+ /**
+ Start a task that will wait for the new key to propagate across relays (see `PacketTunnelActorTimings.wgKeyPropagationDelay`) and then:
+
+ 1. Switch `keyPolicy` back to `.useCurrent`.
+ 2. Reconnect the tunnel using the new key (currently stored in device state)
+ */
+ private func startKeySwitchTask() -> AutoCancellingTask {
+ // Use detached task to prevent inheriting current context.
+ let task = Task.detached { [weak self] in
+ guard let self else { return }
+
+ // Wait for key to propagate across relays.
+ try await Task.sleepUsingContinuousClock(for: timings.wgKeyPropagationDelay)
+
+ // Enqueue task to change key policy.
+ commandChannel.send(.switchKey)
+ }
+
+ return AutoCancellingTask(task)
+ }
+
+ /**
+ Switch key policy from `.usePrior` to `.useCurrent` policy.
+
+ - Returns: `true` if the tunnel should reconnect, otherwise `false`.
+ */
+ private func switchToCurrentKeyInner() -> Bool {
+ switch state {
+ case var .connecting(connState):
+ if setCurrentKeyPolicy(&connState.keyPolicy) {
+ state = .connecting(connState)
+ return true
+ }
+
+ case var .connected(connState):
+ if setCurrentKeyPolicy(&connState.keyPolicy) {
+ state = .connected(connState)
+ return true
+ }
+
+ case var .reconnecting(connState):
+ if setCurrentKeyPolicy(&connState.keyPolicy) {
+ state = .reconnecting(connState)
+ return true
+ }
+
+ case var .error(blockedState):
+ if setCurrentKeyPolicy(&blockedState.keyPolicy) {
+ state = .error(blockedState)
+
+ // Prevent tunnel from reconnecting when in blocked state.
+ return false
+ }
+
+ case .disconnected, .disconnecting, .initial:
+ break
+ }
+ return false
+ }
+
+ /**
+ Internal helper that transitions key policy from `.usePrior` to `.useCurrent`.
+
+ - Parameter keyPolicy: a reference to key policy hend either in connection state or blocked state struct.
+ - Returns: `true` when the policy was modified, otherwise `false`.
+ */
+ private func setCurrentKeyPolicy(_ keyPolicy: inout KeyPolicy) -> Bool {
+ switch keyPolicy {
+ case .useCurrent:
+ return false
+
+ case .usePrior:
+ keyPolicy = .useCurrent
+ return true
+ }
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/Actor+NetworkReachability.swift b/ios/PacketTunnelCore/Actor/Actor+NetworkReachability.swift
new file mode 100644
index 0000000000..34a4e02ed1
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Actor+NetworkReachability.swift
@@ -0,0 +1,79 @@
+//
+// Actor+NetworkReachability.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 26/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension PacketTunnelActor {
+ /**
+ Start observing changes to default path.
+
+ - Parameter notifyObserverWithCurrentPath: immediately notifies path observer with the current path when set to `true`.
+ */
+ func startDefaultPathObserver(notifyObserverWithCurrentPath: Bool = false) {
+ defaultPathObserver.start { [weak self] networkPath in
+ self?.commandChannel.send(.networkReachability(networkPath))
+ }
+
+ if notifyObserverWithCurrentPath, let currentPath = defaultPathObserver.defaultPath {
+ commandChannel.send(.networkReachability(currentPath))
+ }
+ }
+
+ /// Stop observing changes to default path.
+ func stopDefaultPathObserver() {
+ defaultPathObserver.stop()
+ }
+
+ /**
+ Event handler that receives new network path from tunnel monitor and updates internal state with new network reachability status.
+
+ - Parameter networkPath: new default path
+ */
+ func handleDefaultPathChange(_ networkPath: NetworkPath) {
+ let newReachability = networkPath.networkReachability
+
+ func mutateConnectionState(_ connState: inout ConnectionState) -> Bool {
+ if connState.networkReachability != newReachability {
+ connState.networkReachability = newReachability
+ return true
+ }
+ return false
+ }
+
+ switch state {
+ case var .connecting(connState):
+ if mutateConnectionState(&connState) {
+ state = .connecting(connState)
+ }
+
+ case var .connected(connState):
+ if mutateConnectionState(&connState) {
+ state = .connected(connState)
+ }
+
+ case var .reconnecting(connState):
+ if mutateConnectionState(&connState) {
+ state = .reconnecting(connState)
+ }
+
+ case var .disconnecting(connState):
+ if mutateConnectionState(&connState) {
+ state = .disconnecting(connState)
+ }
+
+ case var .error(blockedState):
+ if blockedState.networkReachability != newReachability {
+ blockedState.networkReachability = newReachability
+ state = .error(blockedState)
+ }
+
+ case .initial, .disconnected:
+ break
+ }
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/Actor+Public.swift b/ios/PacketTunnelCore/Actor/Actor+Public.swift
new file mode 100644
index 0000000000..7a50477c29
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Actor+Public.swift
@@ -0,0 +1,59 @@
+//
+// Actor+Public.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 27/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+/**
+ Public methods for dispatching commands to Actor.
+
+ - All methods in this extension are `nonisolated` because the channel they use to pass commands for execution is `nonisolated` too.
+ - FIFO order is guaranteed for all these calls for as long as they are not invoked simultaneously from multiple concurrent queues.
+ - There is no way to wait for these tasks to complete, some of them may even be coalesced and never execute. Observe the `state` to follow the progress.
+ */
+extension PacketTunnelActor {
+ /**
+ Tell actor to start the tunnel.
+
+ - Parameter options: start options.
+ */
+ nonisolated public func start(options: StartOptions) {
+ commandChannel.send(.start(options))
+ }
+
+ /**
+ Tell actor to stop the tunnel.
+ */
+ nonisolated public func stop() {
+ commandChannel.send(.stop)
+ }
+
+ /**
+ Tell actor to reconnect the tunnel.
+
+ - Parameter nextRelay: next relay to connect to.
+ */
+ public nonisolated func reconnect(to nextRelay: NextRelay) {
+ commandChannel.send(.reconnect(nextRelay))
+ }
+
+ /**
+ Tell actor that key rotation took place.
+
+ - Parameter date: date when last key rotation took place.
+ */
+ nonisolated public func notifyKeyRotation(date: Date?) {
+ commandChannel.send(.notifyKeyRotated(date))
+ }
+
+ /**
+ Tell actor to enter error state.
+ */
+ nonisolated public func setErrorState(reason: BlockedStateReason) {
+ commandChannel.send(.error(reason))
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/Actor+SleepCycle.swift b/ios/PacketTunnelCore/Actor/Actor+SleepCycle.swift
new file mode 100644
index 0000000000..c6339ce11e
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Actor+SleepCycle.swift
@@ -0,0 +1,29 @@
+//
+// Actor+SleepCycle.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 26/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension PacketTunnelActor {
+ /**
+ Clients should call this method to notify actor when device wakes up.
+
+ `NEPacketTunnelProvider` provides the corresponding lifecycle method.
+ */
+ public nonisolated func onWake() {
+ tunnelMonitor.onWake()
+ }
+
+ /**
+ Clients should call this method to notify actor when device is about to go to sleep.
+
+ `NEPacketTunnelProvider` provides the corresponding lifecycle method.
+ */
+ public nonisolated func onSleep() {
+ tunnelMonitor.onSleep()
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/Actor.swift b/ios/PacketTunnelCore/Actor/Actor.swift
new file mode 100644
index 0000000000..18be2e2d40
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Actor.swift
@@ -0,0 +1,357 @@
+//
+// Actor.swift
+// PacketTunnel
+//
+// Created by pronebird on 30/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadLogging
+import MullvadTypes
+import NetworkExtension
+import struct RelaySelector.RelaySelectorResult
+import class WireGuardKitTypes.PrivateKey
+
+/**
+ Packet tunnel state machine implemented as an actor.
+
+ - Actor receives commands for execution over the `CommandChannel`.
+
+ - 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.
+
+ - 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
+ same anyway.
+ */
+public actor PacketTunnelActor {
+ @Published internal(set) public var state: State = .initial {
+ didSet {
+ logger.debug("\(state.logFormat())")
+ }
+ }
+
+ let logger = Logger(label: "PacketTunnelActor")
+
+ let timings: PacketTunnelActorTimings
+ let tunnelAdapter: TunnelAdapterProtocol
+ let tunnelMonitor: TunnelMonitorProtocol
+ let defaultPathObserver: DefaultPathObserverProtocol
+ let blockedStateErrorMapper: BlockedStateErrorMapperProtocol
+ let relaySelector: RelaySelectorProtocol
+ let settingsReader: SettingsReaderProtocol
+
+ nonisolated let commandChannel = CommandChannel()
+
+ public init(
+ timings: PacketTunnelActorTimings,
+ tunnelAdapter: TunnelAdapterProtocol,
+ tunnelMonitor: TunnelMonitorProtocol,
+ defaultPathObserver: DefaultPathObserverProtocol,
+ blockedStateErrorMapper: BlockedStateErrorMapperProtocol,
+ relaySelector: RelaySelectorProtocol,
+ settingsReader: SettingsReaderProtocol
+ ) {
+ self.timings = timings
+ self.tunnelAdapter = tunnelAdapter
+ self.tunnelMonitor = tunnelMonitor
+ self.defaultPathObserver = defaultPathObserver
+ self.blockedStateErrorMapper = blockedStateErrorMapper
+ self.relaySelector = relaySelector
+ self.settingsReader = settingsReader
+
+ consumeCommands(channel: commandChannel)
+ }
+
+ deinit {
+ commandChannel.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.
+
+ - Parameter channel: command channel.
+ */
+ private nonisolated func consumeCommands(channel: CommandChannel) {
+ Task.detached { [weak self] in
+ for await command in channel {
+ guard let self else { return }
+
+ self.logger.debug("Received command: \(command.logFormat())")
+
+ switch command {
+ case let .start(options):
+ await start(options: options)
+
+ case .stop:
+ await stop()
+
+ case let .reconnect(nextRelay, stopTunnelMonitor):
+ await reconnect(to: nextRelay, shouldStopTunnelMonitor: stopTunnelMonitor)
+
+ 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)
+ }
+ }
+ }
+ }
+}
+
+// MARK: -
+
+extension PacketTunnelActor {
+ /**
+ Start the tunnel.
+
+ Can only be called once, all subsequent attempts are ignored. Use `reconnect()` if you wish to change relay.
+
+ - Parameter options: start options produced by packet tunnel
+ */
+ private func start(options: StartOptions) async {
+ guard case .initial = state else { return }
+
+ logger.debug("\(options.logFormat())")
+
+ // Start observing default network path to determine network reachability.
+ startDefaultPathObserver()
+
+ // Assign a closure receiving tunnel monitor events.
+ setTunnelMonitorEventHandler()
+
+ do {
+ try await tryStart(nextRelay: options.selectorResult.map { .preSelected($0) } ?? .random)
+ } catch {
+ logger.error(error: error, message: "Failed to start the tunnel.")
+
+ await setErrorStateInternal(with: error)
+ }
+ }
+
+ /// Stop the tunnel.
+ private func stop() async {
+ switch state {
+ case let .connected(connState), let .connecting(connState), let .reconnecting(connState):
+ state = .disconnecting(connState)
+ tunnelMonitor.stop()
+
+ // Fallthrough to stop adapter and shift to `.disconnected` state.
+ fallthrough
+
+ case .error:
+ stopDefaultPathObserver()
+
+ do {
+ try await tunnelAdapter.stop()
+ } catch {
+ logger.error(error: error, message: "Failed to stop adapter.")
+ }
+ state = .disconnected
+
+ case .initial, .disconnected:
+ break
+
+ case .disconnecting:
+ assertionFailure("stop(): out of order execution.")
+ }
+ }
+
+ /**
+ Reconnect tunnel to new relay. Enters error state on failure.
+
+ - Parameters:
+ - nextRelay: next relay to connect to
+ - shouldStopTunnelMonitor: whether tunnel monitor should be stopped
+ */
+ private func reconnect(to nextRelay: NextRelay, shouldStopTunnelMonitor: Bool) async {
+ do {
+ switch state {
+ case .connecting, .connected, .reconnecting, .error:
+ if shouldStopTunnelMonitor {
+ tunnelMonitor.stop()
+ }
+ try await tryStart(nextRelay: nextRelay)
+
+ case .disconnected, .disconnecting, .initial:
+ break
+ }
+ } catch {
+ logger.error(error: error, message: "Failed to reconnect the tunnel.")
+
+ await setErrorStateInternal(with: error)
+ }
+ }
+
+ /**
+ Attempt to start the tunnel by performing the following steps:
+
+ - Read settings.
+ - Determine target state, it can either be `.connecting` or `.reconnecting`. (See `TargetStateForReconnect`)
+ - Bail if target state cannot be determined. That means that the actor is past the point when it could logically connect or reconnect, i.e it can already be in
+ `.disconnecting` state.
+ - Configure tunnel adapter.
+ - Start tunnel monitor.
+ - Reactivate default path observation (disabled when configuring tunnel adapter)
+
+ - Parameter nextRelay: which relay should be selected next.
+ */
+ private func tryStart(nextRelay: NextRelay = .random) async throws {
+ let settings: Settings = try settingsReader.read()
+
+ guard let connectionState = try makeConnectionState(nextRelay: nextRelay, settings: settings),
+ let targetState = state.targetStateForReconnect else { return }
+
+ let activeKey: PrivateKey
+ switch connectionState.keyPolicy {
+ case .useCurrent:
+ activeKey = settings.privateKey
+ case let .usePrior(priorKey, _):
+ activeKey = priorKey
+ }
+
+ switch targetState {
+ case .connecting:
+ state = .connecting(connectionState)
+ case .reconnecting:
+ state = .reconnecting(connectionState)
+ }
+
+ let endpoint = connectionState.selectedRelay.endpoint
+ let configurationBuilder = ConfigurationBuilder(
+ privateKey: activeKey,
+ interfaceAddresses: settings.interfaceAddresses,
+ dns: settings.dnsServers,
+ endpoint: endpoint
+ )
+
+ /*
+ Stop default path observer while updating WireGuard configuration since it will call the system method
+ `NEPacketTunnelProvider.setTunnelNetworkSettings()` which may cause active interfaces to go down making it look
+ like network connectivity is not available, but only for a brief moment.
+ */
+ stopDefaultPathObserver()
+
+ defer {
+ // Restart default path observer and notify the observer with the current path that might have changed while
+ // path observer was paused.
+ startDefaultPathObserver(notifyObserverWithCurrentPath: true)
+ }
+
+ try await tunnelAdapter.start(configuration: configurationBuilder.makeConfiguration())
+
+ // Resume tunnel monitoring and use IPv4 gateway as a probe address.
+ tunnelMonitor.start(probeAddress: endpoint.ipv4Gateway)
+ }
+
+ /**
+ Derive `ConnectionState` from current `state` updating it with new relay and settings.
+
+ - Parameters:
+ - nextRelay: relay preference that should be used when selecting next relay.
+ - settings: current settings
+
+ - Returns: New connection state or `nil` if current state is at or past `.disconnecting` phase.
+ */
+ private func makeConnectionState(nextRelay: NextRelay, settings: Settings) throws -> ConnectionState? {
+ let relayConstraints = settings.relayConstraints
+ let privateKey = settings.privateKey
+
+ switch state {
+ case .initial:
+ return ConnectionState(
+ selectedRelay: try selectRelay(
+ nextRelay: nextRelay,
+ relayConstraints: relayConstraints,
+ currentRelay: nil,
+ connectionAttemptCount: 0
+ ),
+ relayConstraints: relayConstraints,
+ currentKey: privateKey,
+ keyPolicy: .useCurrent,
+ networkReachability: defaultPathObserver.defaultPath?.networkReachability ?? .undetermined,
+ connectionAttemptCount: 0
+ )
+
+ case var .connecting(connState), var .connected(connState), var .reconnecting(connState):
+ connState.selectedRelay = try selectRelay(
+ nextRelay: nextRelay,
+ relayConstraints: relayConstraints,
+ currentRelay: connState.selectedRelay,
+ connectionAttemptCount: connState.connectionAttemptCount
+ )
+ connState.relayConstraints = relayConstraints
+ connState.currentKey = privateKey
+
+ return connState
+
+ case let .error(blockedState):
+ return ConnectionState(
+ selectedRelay: try selectRelay(
+ nextRelay: nextRelay,
+ relayConstraints: relayConstraints,
+ currentRelay: nil,
+ connectionAttemptCount: 0
+ ),
+ relayConstraints: relayConstraints,
+ currentKey: privateKey,
+ keyPolicy: blockedState.keyPolicy,
+ networkReachability: blockedState.networkReachability,
+ connectionAttemptCount: 0,
+ lastKeyRotation: blockedState.lastKeyRotation
+ )
+
+ case .disconnecting, .disconnected:
+ return nil
+ }
+ }
+
+ /**
+ Select next relay to connect to based on `NextRelay` and other input parameters.
+
+ - Parameters:
+ - nextRelay: next relay to connect to.
+ - relayConstraints: relay constraints.
+ - currentRelay: currently selected relay.
+ - connectionAttemptCount: number of failed connection attempts so far.
+
+ - Returns: selector result that contains the credentials of the next relay that the tunnel should connect to.
+ */
+ private func selectRelay(
+ nextRelay: NextRelay,
+ relayConstraints: RelayConstraints,
+ currentRelay: RelaySelectorResult?,
+ connectionAttemptCount: UInt
+ ) throws -> RelaySelectorResult {
+ switch nextRelay {
+ case .current:
+ if let currentRelay {
+ return currentRelay
+ } else {
+ // Fallthrough to .random when current relay is not set.
+ fallthrough
+ }
+
+ case .random:
+ return try relaySelector.selectRelay(
+ with: relayConstraints,
+ connectionAttemptFailureCount: connectionAttemptCount
+ )
+
+ case let .preSelected(selectorResult):
+ return selectorResult
+ }
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/AnyTask.swift b/ios/PacketTunnelCore/Actor/AnyTask.swift
new file mode 100644
index 0000000000..43c44b0644
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/AnyTask.swift
@@ -0,0 +1,17 @@
+//
+// AnyTask.swift
+// PacketTunnel
+//
+// Created by pronebird on 28/08/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+/// A type-erased `Task`.
+public protocol AnyTask {
+ /// Cancel task.
+ func cancel()
+}
+
+extension Task: AnyTask {}
diff --git a/ios/PacketTunnelCore/Actor/AutoCancellingTask.swift b/ios/PacketTunnelCore/Actor/AutoCancellingTask.swift
new file mode 100644
index 0000000000..c80e28f880
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/AutoCancellingTask.swift
@@ -0,0 +1,26 @@
+//
+// AutoCancellingTask.swift
+// PacketTunnel
+//
+// Created by pronebird on 31/08/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+/**
+ Type that cancels the task held inside upon `deinit`.
+
+ It behaves identical to `Combine.AnyCancellable`.
+ */
+public final class AutoCancellingTask {
+ private let task: AnyTask
+
+ init(_ task: AnyTask) {
+ self.task = task
+ }
+
+ deinit {
+ task.cancel()
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/Command.swift b/ios/PacketTunnelCore/Actor/Command.swift
new file mode 100644
index 0000000000..43771e62af
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Command.swift
@@ -0,0 +1,72 @@
+//
+// 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.
+ /// `stopTunnelMonitor = false` is only used when tunnel monitor is paused in response to connectivity loss and shouldn't be stopped explicitly,
+ /// as this would reset its internal counters.
+ case reconnect(NextRelay, stopTunnelMonitor: Bool = true)
+
+ /// 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.relay.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
new file mode 100644
index 0000000000..ca19794b96
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/CommandChannel.swift
@@ -0,0 +1,219 @@
+//
+// CommandChannel.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 27/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+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.
+
+ - 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.
+
+ ### Example
+
+ ```
+ let channel = CommandChannel()
+ 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
+ be consumed until the body of the loop completes the iteration.
+
+ ```
+ Task.detached {
+ for await command in channel {
+ await handleMyCommand(command)
+ }
+ }
+ ```
+
+ Normally channel is expected to be infinite, but it's convenient to end the stream earlier, for instance when testing the coalescing algorithm:
+
+ ```
+ channel.send(.start(..))
+ channel.send(.stop)
+ channel.sendEnd()
+
+ let allReceivedCommands = channel
+ .map { "\($0)" }
+ .reduce(into: [String]()) { $0.append($1) }
+ ```
+ */
+final class CommandChannel: @unchecked Sendable {
+ 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 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] = []
+
+ /// 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()
+
+ init() {}
+
+ 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 }
+
+ buffer.append(value)
+
+ 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
+
+ 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()
+
+ sendEndToPendingContinuations()
+
+ 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)
+ }
+ 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 }
+
+ 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 }
+
+ assert(i < buffer.count)
+ let current = buffer[i]
+
+ // 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
+ }
+ }
+ }
+ }
+ }
+
+ 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 .finished:
+ continuation.resume(returning: nil)
+ }
+ }
+ }
+ }
+}
+
+extension CommandChannel: AsyncSequence {
+ typealias Element = Command
+
+ struct AsyncIterator: AsyncIteratorProtocol {
+ let channel: CommandChannel
+ func next() async -> Command? {
+ return await channel.next()
+ }
+ }
+
+ func makeAsyncIterator() -> AsyncIterator {
+ return AsyncIterator(channel: self)
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift b/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift
new file mode 100644
index 0000000000..c4a731aa78
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift
@@ -0,0 +1,54 @@
+//
+// ConfigurationBuilder.swift
+// PacketTunnel
+//
+// Created by pronebird on 30/08/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import protocol Network.IPAddress
+import struct WireGuardKitTypes.IPAddressRange
+import class WireGuardKitTypes.PrivateKey
+import class WireGuardKitTypes.PublicKey
+
+/// Struct building tunnel adapter configuration.
+struct ConfigurationBuilder {
+ var privateKey: PrivateKey
+ var interfaceAddresses: [IPAddressRange]
+ var dns: SelectedDNSServers?
+ var endpoint: MullvadEndpoint?
+
+ func makeConfiguration() -> TunnelAdapterConfiguration {
+ return TunnelAdapterConfiguration(
+ privateKey: privateKey,
+ interfaceAddresses: interfaceAddresses,
+ dns: dnsServers,
+ peer: peer
+ )
+ }
+
+ private var peer: TunnelPeer? {
+ guard let endpoint else { return nil }
+
+ return TunnelPeer(
+ endpoint: .ipv4(endpoint.ipv4Relay),
+ publicKey: PublicKey(rawValue: endpoint.publicKey)!
+ )
+ }
+
+ private var dnsServers: [IPAddress] {
+ guard let dns else { return [] }
+
+ switch dns {
+ case let .blocking(dnsAddress):
+ return [dnsAddress]
+ case let .custom(customDNSAddresses):
+ return customDNSAddresses
+ case .gateway:
+ guard let endpoint else { return [] }
+ return [endpoint.ipv4Gateway, endpoint.ipv6Gateway]
+ }
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/NetworkPath+NetworkReachability.swift b/ios/PacketTunnelCore/Actor/NetworkPath+NetworkReachability.swift
new file mode 100644
index 0000000000..3ebedfebcf
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/NetworkPath+NetworkReachability.swift
@@ -0,0 +1,28 @@
+//
+// NetworkPath+.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 14/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension NetworkPath {
+ /// Converts `NetworkPath.status` into `NetworkReachability`.
+ var networkReachability: NetworkReachability {
+ switch status {
+ case .satisfiable, .satisfied:
+ return .reachable
+
+ case .unsatisfied:
+ return .unreachable
+
+ case .invalid:
+ return .undetermined
+
+ @unknown default:
+ return .undetermined
+ }
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/Protocols/BlockedStateErrorMapperProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/BlockedStateErrorMapperProtocol.swift
new file mode 100644
index 0000000000..7afb5fc645
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Protocols/BlockedStateErrorMapperProtocol.swift
@@ -0,0 +1,15 @@
+//
+// BlockedStateErrorMapperProtocol.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 14/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+
+/// A type responsible for mapping errors returned by dependencies of `PacketTunnelActor` to `BlockedStateReason`.
+public protocol BlockedStateErrorMapperProtocol {
+ func mapError(_ error: Error) -> BlockedStateReason
+}
diff --git a/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift
new file mode 100644
index 0000000000..b856b2f1fe
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift
@@ -0,0 +1,17 @@
+//
+// RelaySelectorProtocol.swift
+// PacketTunnel
+//
+// Created by pronebird on 08/08/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import RelaySelector
+
+/// Protocol describing a type that can select a relay.
+public protocol RelaySelectorProtocol {
+ func selectRelay(with constraints: RelayConstraints, connectionAttemptFailureCount: UInt) throws
+ -> RelaySelectorResult
+}
diff --git a/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift
new file mode 100644
index 0000000000..dc3bfbcd0f
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift
@@ -0,0 +1,60 @@
+//
+// SettingsReaderProtocol.swift
+// PacketTunnel
+//
+// Created by pronebird on 25/08/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import Network
+import WireGuardKitTypes
+
+/// A type that implements a reader that can return settings required by `PacketTunnelActor` in order to configure the tunnel.
+public protocol SettingsReaderProtocol {
+ /**
+ Read settings from storage.
+
+ - Throws: an error thrown by this method is passed down to the implementation of `BlockedStateErrorMapperProtocol`.
+ - Returns: `Settings` used to configure packet tunnel adapter.
+ */
+ func read() throws -> Settings
+}
+
+/// Struct holding settings necessary to configure packet tunnel adapter.
+public struct Settings {
+ /// Private key used by device.
+ public var privateKey: PrivateKey
+
+ /// IP addresses assigned for tunnel interface.
+ public var interfaceAddresses: [IPAddressRange]
+
+ /// Relay constraints.
+ public var relayConstraints: RelayConstraints
+
+ /// DNS servers selected by user.
+ public var dnsServers: SelectedDNSServers
+
+ public init(
+ privateKey: PrivateKey,
+ interfaceAddresses: [IPAddressRange],
+ relayConstraints: RelayConstraints,
+ dnsServers: SelectedDNSServers
+ ) {
+ self.privateKey = privateKey
+ self.interfaceAddresses = interfaceAddresses
+ self.relayConstraints = relayConstraints
+ self.dnsServers = dnsServers
+ }
+}
+
+/// Enum describing selected DNS servers option.
+public enum SelectedDNSServers {
+ /// Custom DNS servers.
+ case custom([IPAddress])
+ /// Mullvad server acting as a blocking DNS proxy.
+ case blocking(IPAddress)
+ /// Gateway IP will be used as DNS automatically.
+ case gateway
+}
diff --git a/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift
new file mode 100644
index 0000000000..c4c499f83e
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift
@@ -0,0 +1,38 @@
+//
+// TunnelAdapterProtocol.swift
+// PacketTunnel
+//
+// Created by pronebird on 08/08/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import Network
+
+import struct WireGuardKitTypes.IPAddressRange
+import class WireGuardKitTypes.PrivateKey
+import class WireGuardKitTypes.PublicKey
+
+/// Protocol describing interface for any kind of adapter implementing a VPN tunnel.
+public protocol TunnelAdapterProtocol {
+ /// Start tunnel adapter or update active configuration.
+ func start(configuration: TunnelAdapterConfiguration) async throws
+
+ /// Stop tunnel adapter with the given configuration.
+ func stop() async throws
+}
+
+/// Struct describing tunnel adapter configuration.
+public struct TunnelAdapterConfiguration {
+ public var privateKey: PrivateKey
+ public var interfaceAddresses: [IPAddressRange]
+ public var dns: [IPAddress]
+ public var peer: TunnelPeer?
+}
+
+/// Struct describing a single peer.
+public struct TunnelPeer {
+ public var endpoint: AnyIPEndpoint
+ public var publicKey: PublicKey
+}
diff --git a/ios/PacketTunnel/StartOptions.swift b/ios/PacketTunnelCore/Actor/StartOptions.swift
index 773c2e3430..d2ebe72611 100644
--- a/ios/PacketTunnel/StartOptions.swift
+++ b/ios/PacketTunnelCore/Actor/StartOptions.swift
@@ -10,12 +10,21 @@ import Foundation
import RelaySelector
/// Packet tunnel start options parsed from dictionary passed to packet tunnel with a call to `startTunnel()`.
-struct StartOptions {
- var launchSource: LaunchSource
- var selectorResult: RelaySelectorResult?
+public struct StartOptions {
+ /// The system that triggered the launch of packet tunnel.
+ public var launchSource: LaunchSource
+
+ /// Pre-selected relay received from UI when available.
+ public var selectorResult: RelaySelectorResult?
+
+ /// Designated initializer.
+ public init(launchSource: LaunchSource, selectorResult: RelaySelectorResult? = nil) {
+ self.launchSource = launchSource
+ self.selectorResult = selectorResult
+ }
/// Returns a brief description suitable for output to tunnel provider log.
- func logFormat() -> String {
+ public func logFormat() -> String {
var s = "Start the tunnel via \(launchSource)"
if let selectorResult {
s += ", connect to \(selectorResult.relay.hostname)"
@@ -26,7 +35,7 @@ struct StartOptions {
}
/// The source facility that triggered a launch of packet tunnel extension.
-enum LaunchSource: String, CustomStringConvertible {
+public enum LaunchSource: String, CustomStringConvertible {
/// Launched by the main bundle app using network extension framework.
case app
@@ -37,7 +46,7 @@ enum LaunchSource: String, CustomStringConvertible {
case system
/// Returns a human readable description of launch source.
- var description: String {
+ public var description: String {
switch self {
case .app, .system:
return rawValue
diff --git a/ios/PacketTunnelCore/Actor/State+Extensions.swift b/ios/PacketTunnelCore/Actor/State+Extensions.swift
new file mode 100644
index 0000000000..cfaed6cf07
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/State+Extensions.swift
@@ -0,0 +1,112 @@
+//
+// State+.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 08/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import class WireGuardKitTypes.PrivateKey
+
+extension State {
+ /// Returns the target state to which the actor state should transition when requested to reconnect.
+ /// It returns `nil` when reconnection is not supported such as when already `.disconnecting` or `.disconnected` states.
+ var targetStateForReconnect: TargetStateForReconnect? {
+ switch self {
+ case .initial:
+ return .connecting
+
+ case .connecting:
+ return .connecting
+
+ case .connected, .reconnecting:
+ return .reconnecting
+
+ case let .error(blockedState):
+ switch blockedState.priorState {
+ case .initial, .connecting:
+ return .connecting
+ case .connected, .reconnecting:
+ return .reconnecting
+ }
+
+ case .disconnecting, .disconnected:
+ return nil
+ }
+ }
+
+ // MARK: - Logging
+
+ func logFormat() -> String {
+ switch self {
+ case let .connecting(connState), let .connected(connState), let .reconnecting(connState):
+ let hostname = connState.selectedRelay.relay.hostname
+
+ return """
+ \(name) to \(hostname), \
+ key: \(connState.keyPolicy.logFormat()), \
+ net: \(connState.networkReachability), \
+ attempt: \(connState.connectionAttemptCount)
+ """
+
+ case let .error(blockedState):
+ return "\(name): \(blockedState.reason)"
+
+ case .initial, .disconnecting, .disconnected:
+ return name
+ }
+ }
+
+ var name: String {
+ switch self {
+ case .connected:
+ return "Connected"
+ case .connecting:
+ return "Connecting"
+ case .reconnecting:
+ return "Reconnecting"
+ case .disconnecting:
+ return "Disconnecting"
+ case .disconnected:
+ return "Disconnected"
+ case .initial:
+ return "Initial"
+ case .error:
+ return "Error"
+ }
+ }
+}
+
+extension KeyPolicy {
+ func logFormat() -> String {
+ switch self {
+ case .useCurrent:
+ return "current"
+ case .usePrior:
+ return "prior"
+ }
+ }
+}
+
+extension BlockedStateReason {
+ /**
+ Returns true if the tunnel should attempt to restart periodically to recover from error that does not require explicit restart to be initiated by user.
+
+ Common scenarios when tunnel will attempt to restart itself periodically:
+
+ - Keychain and filesystem are locked on boot until user unlocks device in the very first time.
+ - App update that requires settings schema migration. Packet tunnel will be automatically restarted after update but it would not be able to read settings until
+ user opens the app which performs migration.
+ */
+ var shouldRestartAutomatically: Bool {
+ switch self {
+ case .deviceLocked:
+ return true
+
+ case .noRelaysSatisfyingConstraints, .readSettings, .invalidAccount, .deviceRevoked, .tunnelAdapter, .unknown,
+ .deviceLoggedOut, .outdatedSchema:
+ return false
+ }
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift
new file mode 100644
index 0000000000..2c5cfd13b8
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/State.swift
@@ -0,0 +1,215 @@
+//
+// States.swift
+// PacketTunnel
+//
+// Created by pronebird on 07/08/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import struct RelaySelector.RelaySelectorResult
+import TunnelObfuscation
+import class WireGuardKitTypes.PrivateKey
+
+/**
+ Tunnel actor state with metadata describing the current phase of packet tunnel lifecycle.
+
+ ## General lifecycle
+
+ Packet tunnel always begins in `.initial` state and ends `.disconnected` state over time. Packet tunnel process is not recycled and hence once
+ the `.disconnected` state is reached, the process is terminated. The new process is started next time VPN is activated.
+
+ ```
+ .initial → .connecting → .connected → .disconnecting → .disconnected
+ ```
+
+ ## Reconnecting state
+
+ `.reconnecting` can be entered from `.connected` state.
+
+ ```
+ .connected → .reconnecting -> .connected
+ .reconnecting → .disconnecting → .disconnected
+ ```
+
+ ## Error state
+
+ `.error` can be entered from nearly any other state except when the tunnel is at or past `.disconnecting` phase.
+
+ A call to reconnect the tunnel while in error state can be used to attempt the recovery and exit the error state upon success.
+ Note that actor decides the target state when transitioning from `.error` state forward based on state prior to error state.
+
+ ```
+ .error → .reconnecting
+ .error → .connecting
+ ```
+
+ ### Packet tunnel considerations
+
+ Packet tunnel should raise `NEPacketTunnelProvider.reasserting` when `reconnecting` but not when `connecting` since
+ `reasserting = false` always leads to `NEVPNStatus.connected`.
+
+ ## Interruption
+
+ `.connecting`, `.reconnecting`, `.error` can be interrupted if the tunnel is requested to stop, which should segue actor towards `.disconnected` state.
+
+ */
+public enum State {
+ /// Initial state at the time when actor is initialized but before the first connection attempt.
+ case initial
+
+ /// Tunnel is attempting to connect.
+ /// The actor should remain in this state until the very first connection is established, i.e determined by tunnel monitor.
+ case connecting(ConnectionState)
+
+ /// Tunnel is connected.
+ case connected(ConnectionState)
+
+ /// Tunnel is attempting to reconnect.
+ case reconnecting(ConnectionState)
+
+ /// Tunnel is disconnecting.
+ case disconnecting(ConnectionState)
+
+ /// Tunnel is disconnected.
+ /// Normally the process is shutdown after entering this state.
+ case disconnected
+
+ /// Error state.
+ /// This state is normally entered when the tunnel is unable to start or reconnect.
+ /// In this state the tunnel blocks all nework connectivity by setting up a peerless WireGuard tunnel, and either awaits user action or, in certain
+ /// circumstances, attempts to recover automatically using a repeating timer.
+ case error(BlockedState)
+}
+
+/// Policy describing what WG key to use for tunnel communication.
+public enum KeyPolicy {
+ /// Use current key stored in device data.
+ case useCurrent
+
+ /// Use prior key until timer fires.
+ case usePrior(_ priorKey: PrivateKey, _ timerTask: AutoCancellingTask)
+}
+
+/// Enum describing network availability.
+public enum NetworkReachability: Equatable {
+ case undetermined, reachable, unreachable
+}
+
+/// Data associated with states that hold connection data.
+public struct ConnectionState {
+ /// Current selected relay.
+ public var selectedRelay: RelaySelectorResult
+
+ /// Last relay constraints read from settings.
+ /// This is primarily used by packet tunnel for updating constraints in tunnel provider.
+ public var relayConstraints: RelayConstraints
+
+ /// Last WG key read from setings.
+ /// Can be `nil` if moved to `keyPolicy`.
+ public var currentKey: PrivateKey?
+
+ /// Policy describing the current key that should be used by the tunnel.
+ public var keyPolicy: KeyPolicy
+
+ /// Whether network connectivity outside of tunnel is available.
+ public var networkReachability: NetworkReachability
+
+ /// Connection attempt counter.
+ /// Reset to zero once connection is established.
+ public var connectionAttemptCount: UInt
+
+ /// Last time packet tunnel rotated the key.
+ public var lastKeyRotation: Date?
+
+ /// Increment connection attempt counter by one, wrapping to zero on overflow.
+ public mutating func incrementAttemptCount() {
+ let (value, isOverflow) = connectionAttemptCount.addingReportingOverflow(1)
+ connectionAttemptCount = isOverflow ? 0 : value
+ }
+}
+
+/// Data associated with error state.
+public struct BlockedState {
+ /// Reason why block state was entered.
+ public var reason: BlockedStateReason
+
+ /// Last relay constraints read from settings.
+ /// This is primarily used by packet tunnel for updating constraints in tunnel provider.
+ public var relayConstraints: RelayConstraints?
+
+ /// Last WG key read from setings.
+ /// Can be `nil` if moved to `keyPolicy` or when it's uknown.
+ public var currentKey: PrivateKey?
+
+ /// Policy describing the current key that should be used by the tunnel.
+ public var keyPolicy: KeyPolicy
+
+ /// Whether network connectivity outside of tunnel is available.
+ public var networkReachability: NetworkReachability
+
+ /// Last time packet tunnel rotated or attempted to rotate the key.
+ /// This is used by `TunnelManager` to detect when it needs to refresh device state from Keychain.
+ public var lastKeyRotation: Date?
+
+ /// Task responsible for periodically calling actor to restart the tunnel.
+ /// Initiated based on the error that led to blocked state.
+ public var recoveryTask: AutoCancellingTask?
+
+ /// Prior state of the actor before entering blocked state
+ public var priorState: StatePriorToBlockedState
+}
+
+/// Reason why packet tunnel entered error state.
+public enum BlockedStateReason: String, Codable, Equatable {
+ /// Device is locked.
+ case deviceLocked
+
+ /// Settings schema is outdated.
+ case outdatedSchema
+
+ /// No relay satisfying constraints.
+ case noRelaysSatisfyingConstraints
+
+ /// Any other failure when reading settings.
+ case readSettings
+
+ /// Invalid account.
+ case invalidAccount
+
+ /// Device revoked.
+ case deviceRevoked
+
+ /// Device is logged out.
+ /// This is an extreme edge case, most likely means that main bundle forgot to delete the VPN configuration during logout.
+ case deviceLoggedOut
+
+ /// Tunnel adapter error.
+ case tunnelAdapter
+
+ /// Unidentified reason.
+ case unknown
+}
+
+/// Legal states that can precede error state.
+public enum StatePriorToBlockedState {
+ case initial, connecting, connected, reconnecting
+}
+
+/// Target state the actor should transition into upon request to either start (connect) or reconnect.
+public enum TargetStateForReconnect {
+ case reconnecting, connecting
+}
+
+/// Describes which relay the tunnel should connect to next.
+public enum NextRelay: Equatable {
+ /// Select next relay randomly.
+ case random
+
+ /// Use currently selected relay, fallback to random if not set.
+ case current
+
+ /// Use pre-selected relay.
+ case preSelected(RelaySelectorResult)
+}
diff --git a/ios/PacketTunnelCore/Actor/Task+Duration.swift b/ios/PacketTunnelCore/Actor/Task+Duration.swift
new file mode 100644
index 0000000000..32f7a3d866
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Task+Duration.swift
@@ -0,0 +1,56 @@
+//
+// Task+.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 11/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+
+private typealias TaskCancellationError = CancellationError
+
+extension Task where Success == Never, Failure == Never {
+ /**
+ Suspends the current task for at least the given duration.
+
+ Negative durations are clamped to zero.
+
+ - Parameter duration: duration that determines how long the task should be suspended.
+ */
+ @available(iOS, introduced: 14.0, obsoleted: 16.0, message: "Replace with Task.sleep(for:tolerance:clock:).")
+ static func sleep(duration: Duration) async throws {
+ let millis = UInt64(max(0, duration.milliseconds))
+ let nanos = millis.saturatingMultiplication(1_000_000)
+
+ try await Task.sleep(nanoseconds: nanos)
+ }
+
+ /**
+ Suspends the current task for the given duration.
+
+ Negative durations are clamped to zero.
+
+ - Parameter duration: duration that determines how long the task should be suspended.
+ */
+ @available(iOS, introduced: 14.0, obsoleted: 16.0, message: "Replace with Task.sleep(for:tolerance:clock:).")
+ static func sleepUsingContinuousClock(for duration: Duration) async throws {
+ let timer = DispatchSource.makeTimerSource()
+
+ try await withTaskCancellationHandler {
+ try await withCheckedThrowingContinuation { continuation in
+ timer.setEventHandler {
+ continuation.resume()
+ }
+ timer.setCancelHandler {
+ continuation.resume(throwing: TaskCancellationError())
+ }
+ timer.schedule(wallDeadline: .now() + DispatchTimeInterval.milliseconds(duration.milliseconds))
+ timer.activate()
+ }
+ } onCancel: {
+ timer.cancel()
+ }
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/Timings.swift b/ios/PacketTunnelCore/Actor/Timings.swift
new file mode 100644
index 0000000000..4e2d5d8b77
--- /dev/null
+++ b/ios/PacketTunnelCore/Actor/Timings.swift
@@ -0,0 +1,28 @@
+//
+// Timings.swift
+// PacketTunnelCore
+//
+// Created by pronebird on 21/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+
+/// Struct holding all timings used by tunnel actor.
+public struct PacketTunnelActorTimings {
+ /// Periodicity at which actor will attempt to restart when an error occurred on system boot when filesystem is locked until device is unlocked.
+ public var bootRecoveryPeriodicity: Duration
+
+ /// Time that takes for new WireGuard key to propagate across relays.
+ public var wgKeyPropagationDelay: Duration
+
+ /// Designated initializer.
+ public init(
+ bootRecoveryPeriodicity: Duration = .seconds(10),
+ wgKeyPropagationDelay: Duration = .seconds(120)
+ ) {
+ self.bootRecoveryPeriodicity = bootRecoveryPeriodicity
+ self.wgKeyPropagationDelay = wgKeyPropagationDelay
+ }
+}
diff --git a/ios/PacketTunnelCore/IPC/PacketTunnelErrorWrapper.swift b/ios/PacketTunnelCore/IPC/PacketTunnelErrorWrapper.swift
deleted file mode 100644
index 381b64898a..0000000000
--- a/ios/PacketTunnelCore/IPC/PacketTunnelErrorWrapper.swift
+++ /dev/null
@@ -1,49 +0,0 @@
-//
-// PacketTunnelErrorWrapper.swift
-// MullvadTypes
-//
-// Created by Sajad Vishkai on 2022-11-28.
-// Copyright © 2022 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-public enum PacketTunnelErrorWrapper: Codable, Equatable, LocalizedError {
- public enum ConfigurationFailureCause: Codable, Equatable {
- /// Device is locked.
- case deviceLocked
-
- /// Settings schema is outdated.
- case outdatedSchema
-
- /// No relay satisfying constraints.
- case noRelaysSatisfyingConstraints
-
- /// Read error.
- case readFailure
- }
-
- /// Failure that indicates WireGuard errors.
- case wireguard(String)
-
- /// Failure to read stored settings.
- case configuration(ConfigurationFailureCause)
-
- public var errorDescription: String? {
- switch self {
- case let .wireguard(error):
- return error
- case let .configuration(cause):
- switch cause {
- case .deviceLocked:
- return "Device is locked."
- case .outdatedSchema:
- return "Settings schema is outdated."
- case .readFailure:
- return "Failure to read VPN configuration."
- case .noRelaysSatisfyingConstraints:
- return "No relays satisfying constraints."
- }
- }
- }
-}
diff --git a/ios/PacketTunnelCore/IPC/PacketTunnelStatus.swift b/ios/PacketTunnelCore/IPC/PacketTunnelStatus.swift
index 5eece926a6..f18f551037 100644
--- a/ios/PacketTunnelCore/IPC/PacketTunnelStatus.swift
+++ b/ios/PacketTunnelCore/IPC/PacketTunnelStatus.swift
@@ -9,86 +9,17 @@
import Foundation
import MullvadTypes
-/// The verdict of an account status check.
-public enum AccountVerdict: Equatable, Codable {
- /// Account is no longer valid.
- case invalid
-
- /// Account is expired.
- case expired(Account)
-
- /// Account exists and has enough time left.
- case active(Account)
-}
-
-/// The verdict of a device status check.
-public enum DeviceVerdict: Equatable, Codable {
- /// Device is revoked.
- case revoked
-
- /// Device exists but the public key registered on server does not match any longer.
- case keyMismatch
-
- /// Device is in good standing and should work as normal.
- case active
-}
-
-/// Type describing whether key rotation took place and the outcome of it.
-public enum KeyRotationStatus: Equatable, Codable {
- /// No rotation took place yet.
- case noAction
-
- /// Rotation attempt took place but without success.
- case attempted(Date)
-
- /// Rotation attempt took place and succeeded.
- case succeeded(Date)
-
- /// Returns `true` if the status is `.succeeded`.
- public var isSucceeded: Bool {
- if case .succeeded = self {
- return true
- } else {
- return false
- }
- }
-}
-
-/**
- Struct holding data associated with account and device diagnostics and also device key recovery performed by packet
- tunnel process.
- */
-public struct DeviceCheck: Codable, Equatable {
- /// The verdict of account status check.
- public var accountVerdict: AccountVerdict
-
- /// The verdict of device status check.
- public var deviceVerdict: DeviceVerdict
-
- // The status of the last performed key rotation.
- public var keyRotationStatus: KeyRotationStatus
-
- public init(
- accountVerdict: AccountVerdict,
- deviceVerdict: DeviceVerdict,
- keyRotationStatus: KeyRotationStatus
- ) {
- self.accountVerdict = accountVerdict
- self.deviceVerdict = deviceVerdict
- self.keyRotationStatus = keyRotationStatus
- }
-}
-
/// Struct describing packet tunnel process status.
public struct PacketTunnelStatus: Codable, Equatable {
- /// Last tunnel error.
- public var lastErrors: [PacketTunnelErrorWrapper]
+ /// The reason why packet tunnel entered error state.
+ /// Set to `nil` when tunnel is not in error state.
+ public var blockedStateReason: BlockedStateReason?
/// Flag indicating whether network is reachable.
public var isNetworkReachable: Bool
- /// Last performed device check.
- public var deviceCheck: DeviceCheck?
+ /// The date of last performed key rotation during device check.
+ public var lastKeyRotation: Date?
/// Current relay.
public var tunnelRelay: PacketTunnelRelay?
@@ -97,15 +28,15 @@ public struct PacketTunnelStatus: Codable, Equatable {
public var numberOfFailedAttempts: UInt
public init(
- lastErrors: [PacketTunnelErrorWrapper] = [],
+ blockStateReason: BlockedStateReason? = nil,
isNetworkReachable: Bool = true,
- deviceCheck: DeviceCheck? = nil,
+ lastKeyRotation: Date? = nil,
tunnelRelay: PacketTunnelRelay? = nil,
numberOfFailedAttempts: UInt = 0
) {
- self.lastErrors = lastErrors
+ self.blockedStateReason = blockStateReason
self.isNetworkReachable = isNetworkReachable
- self.deviceCheck = deviceCheck
+ self.lastKeyRotation = lastKeyRotation
self.tunnelRelay = tunnelRelay
self.numberOfFailedAttempts = numberOfFailedAttempts
}
diff --git a/ios/PacketTunnelCore/Pinger/Pinger.swift b/ios/PacketTunnelCore/Pinger/Pinger.swift
index 0d754f3cfa..f5a0cee0b5 100644
--- a/ios/PacketTunnelCore/Pinger/Pinger.swift
+++ b/ios/PacketTunnelCore/Pinger/Pinger.swift
@@ -11,6 +11,7 @@ import protocol Network.IPAddress
import struct Network.IPv4Address
import struct Network.IPv6Address
+/// ICMP client.
public final class Pinger: PingerProtocol {
// Socket read buffer size.
private static let bufferSize = 65535
diff --git a/ios/PacketTunnelCore/Pinger/PingerProtocol.swift b/ios/PacketTunnelCore/Pinger/PingerProtocol.swift
index 9df205ab6d..2f1c9d544c 100644
--- a/ios/PacketTunnelCore/Pinger/PingerProtocol.swift
+++ b/ios/PacketTunnelCore/Pinger/PingerProtocol.swift
@@ -9,16 +9,25 @@
import Foundation
import Network
+/// The result of processing ICMP reply.
public enum PingerReply {
+ /// ICMP reply was successfully parsed.
case success(_ sender: IPAddress, _ sequenceNumber: UInt16)
+
+ /// ICMP reply couldn't be parsed.
case parseError(Error)
}
+/// The result of sending ICMP echo.
public struct PingerSendResult {
+ /// Sequence id.
public var sequenceNumber: UInt16
+
+ /// How many bytes were sent.
public var bytesSent: UInt
}
+/// A type capable of sending and receving ICMP traffic.
public protocol PingerProtocol {
var onReply: ((PingerReply) -> Void)? { get set }
diff --git a/ios/PacketTunnelCore/TunnelMonitor/DefaultPathObserverProtocol.swift b/ios/PacketTunnelCore/TunnelMonitor/DefaultPathObserverProtocol.swift
index 97b31a1683..d4d192fb74 100644
--- a/ios/PacketTunnelCore/TunnelMonitor/DefaultPathObserverProtocol.swift
+++ b/ios/PacketTunnelCore/TunnelMonitor/DefaultPathObserverProtocol.swift
@@ -9,17 +9,20 @@
import Foundation
import NetworkExtension
+/// A type providing default path access and observation.
public protocol DefaultPathObserverProtocol {
/// Returns current default path or `nil` if unknown yet.
var defaultPath: NetworkPath? { get }
/// Start observing changes to `defaultPath`.
+ /// This call must be idempotent. Multiple calls to start should replace the existing handler block.
func start(_ body: @escaping (NetworkPath) -> Void)
/// Stop observing changes to `defaultPath`.
func stop()
}
+/// A type that represents a network path.
public protocol NetworkPath {
var status: NetworkExtension.NWPathStatus { get }
}
diff --git a/ios/PacketTunnelCore/TunnelMonitor/TunnelDeviceInfoProtocol.swift b/ios/PacketTunnelCore/TunnelMonitor/TunnelDeviceInfoProtocol.swift
index fcd1ff88c3..829265c607 100644
--- a/ios/PacketTunnelCore/TunnelMonitor/TunnelDeviceInfoProtocol.swift
+++ b/ios/PacketTunnelCore/TunnelMonitor/TunnelDeviceInfoProtocol.swift
@@ -8,6 +8,7 @@
import Foundation
+/// A type that can provide statistics and basic information about tunnel device.
public protocol TunnelDeviceInfoProtocol {
/// 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 576a29533e..195a8acde8 100644
--- a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift
+++ b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift
@@ -12,6 +12,7 @@ import MullvadTypes
import protocol Network.IPAddress
import struct Network.IPv4Address
+/// Tunnel monitor.
public final class TunnelMonitor: TunnelMonitorProtocol {
/// Connection state.
private enum ConnectionState {
@@ -459,11 +460,9 @@ public final class TunnelMonitor: TunnelMonitorProtocol {
if isReachable {
logger.debug("Start monitoring connection.")
startMonitoring()
- sendNetworkStatusChangeEvent(true)
} else {
logger.debug("Wait for network to become reachable before starting monitoring.")
state.connectionState = .waitingConnectivity
- sendNetworkStatusChangeEvent(false)
}
case .waitingConnectivity:
@@ -471,7 +470,6 @@ public final class TunnelMonitor: TunnelMonitorProtocol {
logger.debug("Network is reachable. Resume monitoring.")
startMonitoring()
- sendNetworkStatusChangeEvent(true)
case .connecting, .connected:
guard !isReachable else { return }
@@ -479,7 +477,6 @@ public final class TunnelMonitor: TunnelMonitorProtocol {
logger.debug("Network is unreachable. Pause monitoring.")
state.connectionState = .waitingConnectivity
stopMonitoring(resetRetryAttempt: true)
- sendNetworkStatusChangeEvent(false)
case .stopped, .recovering:
break
@@ -588,12 +585,6 @@ public final class TunnelMonitor: TunnelMonitorProtocol {
}
}
- private func sendNetworkStatusChangeEvent(_ isNetworkReachable: Bool) {
- eventQueue.async {
- self.onEvent?(.networkReachabilityChanged(isNetworkReachable))
- }
- }
-
private enum ConnectionEvaluation {
case ok
case sendInitialPing
diff --git a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift
index 3bee394242..bae8250d27 100644
--- a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift
+++ b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift
@@ -17,12 +17,10 @@ public enum TunnelMonitorEvent {
/// Dispatched when connection stops receiving ping responses.
/// The handler is responsible to reconfigure the tunnel and call `TunnelMonitorProtocol.start(probeAddress:)` to resume connection monitoring.
case connectionLost
-
- /// Dispatched when network reachability changes.
- case networkReachabilityChanged(_ isNetworkReachable: Bool)
}
-public protocol TunnelMonitorProtocol {
+/// A type that can provide tunnel monitoring.
+public protocol TunnelMonitorProtocol: AnyObject {
/// Event handler that starts receiving events after the call to `start(probeAddress:)`.
var onEvent: ((TunnelMonitorEvent) -> Void)? { get set }
diff --git a/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxy.swift b/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxy.swift
index a5b3be3bc2..e6f6b38e15 100644
--- a/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxy.swift
+++ b/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxy.swift
@@ -11,7 +11,8 @@ import MullvadREST
import MullvadTransport
import MullvadTypes
-public final class URLRequestProxy {
+/// Network request proxy capable of passing serializable requests and responses over the given transport provider.
+public final class URLRequestProxy: @unchecked Sendable {
/// Serial queue used for synchronizing access to class members.
private let dispatchQueue: DispatchQueue
@@ -30,14 +31,17 @@ public final class URLRequestProxy {
public func sendRequest(
_ proxyRequest: ProxyURLRequest,
- completionHandler: @escaping (ProxyURLResponse) -> Void
+ completionHandler: @escaping @Sendable (ProxyURLResponse) -> Void
) {
dispatchQueue.async {
- guard let transportProvider = self.transportProvider.makeTransport() else { return }
+ guard let transportProvider = self.transportProvider.makeTransport() else {
+ // Edge case in which case we return `ProxyURLResponse` with no data.
+ completionHandler(ProxyURLResponse(data: nil, response: nil, error: nil))
+ return
+ }
// The task sent by `transport.sendRequest` comes in an already resumed state
- let task = transportProvider.sendRequest(proxyRequest.urlRequest) { [weak self] data, response, error in
- guard let self else { return }
+ let task = transportProvider.sendRequest(proxyRequest.urlRequest) { [self] data, response, error in
// However there is no guarantee about which queue the execution resumes on
// Use `dispatchQueue` to guarantee thread safe access to `proxiedRequests`
dispatchQueue.async {
@@ -73,3 +77,13 @@ public final class URLRequestProxy {
return proxiedRequests.removeValue(forKey: identifier)
}
}
+
+extension URLRequestProxy {
+ public func sendRequest(_ proxyRequest: ProxyURLRequest) async -> ProxyURLResponse {
+ return await withCheckedContinuation { continuation in
+ sendRequest(proxyRequest) { proxyResponse in
+ continuation.resume(returning: proxyResponse)
+ }
+ }
+ }
+}
diff --git a/ios/PacketTunnelCoreTests/ActorTests.swift b/ios/PacketTunnelCoreTests/ActorTests.swift
new file mode 100644
index 0000000000..7b8cd69538
--- /dev/null
+++ b/ios/PacketTunnelCoreTests/ActorTests.swift
@@ -0,0 +1,102 @@
+//
+// ActorTests.swift
+// PacketTunnelCoreTests
+//
+// Created by pronebird on 05/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Combine
+import MullvadSettings
+import MullvadTypes
+import Network
+@testable import PacketTunnelCore
+@testable import RelaySelector
+import struct WireGuardKitTypes.IPAddressRange
+import class WireGuardKitTypes.PrivateKey
+import XCTest
+
+final class ActorTests: XCTestCase {
+ private var actor: PacketTunnelActor?
+ private var stateSink: Combine.Cancellable?
+
+ override func tearDown() async throws {
+ stateSink?.cancel()
+ actor?.stop()
+ await actor?.waitUntilDisconnected()
+ }
+
+ /**
+ Test a happy path start sequence.
+
+ As actor should transition through the following states: .initial → .connecting → .connected
+ */
+ func testStart() async throws {
+ let actor = PacketTunnelActor.mock()
+ let initialStateExpectation = expectation(description: "Expect initial state")
+ let connectingExpectation = expectation(description: "Expect connecting state")
+ let connectedStateExpectation = expectation(description: "Expect connected state")
+
+ let allExpectations = [initialStateExpectation, connectingExpectation, connectedStateExpectation]
+
+ stateSink = await actor.$state
+ .receive(on: DispatchQueue.main)
+ .sink { newState in
+ switch newState {
+ case .initial:
+ initialStateExpectation.fulfill()
+ case .connecting:
+ connectingExpectation.fulfill()
+ case .connected:
+ connectedStateExpectation.fulfill()
+ default:
+ break
+ }
+ }
+
+ self.actor = actor
+
+ actor.start(options: StartOptions(launchSource: .app))
+
+ await fulfillment(of: allExpectations, timeout: 1, enforceOrder: true)
+ }
+
+ /**
+ Test stopping connected tunnel.
+
+ As actor should transition through the following states: .connected → .disconnecting → .disconnected
+ */
+ func testStopConnectedTunnel() async throws {
+ let actor = PacketTunnelActor.mock()
+ let connectedStateExpectation = expectation(description: "Expect connected state")
+ let disconnectingStateExpectation = expectation(description: "Expect disconnecting state")
+ let disconnectedStateExpectation = expectation(description: "Expect disconnected state")
+
+ let allExpectations = [connectedStateExpectation, disconnectingStateExpectation, disconnectedStateExpectation]
+
+ stateSink = await actor.$state
+ .receive(on: DispatchQueue.main)
+ .sink { newState in
+ switch newState {
+ case .connected:
+ connectedStateExpectation.fulfill()
+ actor.stop()
+
+ case .disconnecting:
+ disconnectingStateExpectation.fulfill()
+
+ case .disconnected:
+ disconnectedStateExpectation.fulfill()
+
+ default:
+ break
+ }
+ }
+
+ self.actor = actor
+
+ actor.start(options: StartOptions(launchSource: .app))
+
+ await fulfillment(of: allExpectations, timeout: 1, enforceOrder: true)
+ }
+}
diff --git a/ios/PacketTunnelCoreTests/CommandChannelTests.swift b/ios/PacketTunnelCoreTests/CommandChannelTests.swift
new file mode 100644
index 0000000000..dc622434b9
--- /dev/null
+++ b/ios/PacketTunnelCoreTests/CommandChannelTests.swift
@@ -0,0 +1,109 @@
+//
+// CommandChannelTests.swift
+// PacketTunnelCoreTests
+//
+// Created by pronebird on 27/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+@testable import PacketTunnelCore
+import XCTest
+
+final class CommandChannelTests: XCTestCase {
+ func testCoalescingReconnect() async {
+ let channel = CommandChannel()
+
+ channel.send(.start(StartOptions(launchSource: .app)))
+ channel.send(.reconnect(.random))
+ channel.send(.reconnect(.random))
+ channel.send(.switchKey)
+ channel.send(.reconnect(.current))
+ channel.sendEnd()
+
+ let commands = await channel.map { $0.primitiveCommand }.collect()
+
+ XCTAssertEqual(commands, [.start, .switchKey, .reconnect(.current)])
+ }
+
+ /// Test that stops cancels all preceding tasks.
+ func testCoalescingStop() async {
+ let channel = CommandChannel()
+
+ channel.send(.start(StartOptions(launchSource: .app)))
+ channel.send(.reconnect(.random))
+ channel.send(.stop)
+ channel.send(.reconnect(.current))
+ channel.send(.stop)
+ channel.send(.switchKey)
+ channel.sendEnd()
+
+ let commands = await channel.map { $0.primitiveCommand }.collect()
+
+ XCTAssertEqual(commands, [.stop, .switchKey])
+ }
+
+ /// Test that iterations over the finished channel yield `nil`.
+ func testFinishFlushingUnconsumedValues() async {
+ let channel = CommandChannel()
+ channel.send(.stop)
+ channel.finish()
+
+ let value = await channel.makeAsyncIterator().next()
+ XCTAssertNil(value)
+ }
+
+ /// Test that the call to `finish()` ends the iteration that began prior to that.
+ func testFinishEndsAsyncIterator() async throws {
+ let channel = CommandChannel()
+ 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.
+ Task {
+ for await command in channel {
+ print(command)
+ }
+
+ expectEndIteration.fulfill()
+ }
+
+ // Tell channel to finish() after a small delay. This should resume execution in the task above and exit the
+ // for-await loop.
+ Task {
+ try await Task.sleep(nanoseconds: 1_000_000)
+
+ expectFinish.fulfill()
+ channel.finish()
+ }
+
+ await fulfillment(of: [expectFinish, expectEndIteration], timeout: 100, enforceOrder: true)
+ }
+}
+
+extension AsyncSequence {
+ func collect() async rethrows -> [Element] {
+ try await reduce(into: [Element]()) { $0.append($1) }
+ }
+}
+
+/// Primitive version of `Command` that can be used in tests and easily compared against.
+enum PrimitiveCommand: Equatable {
+ case start, stop, reconnect(NextRelay), switchKey, other
+}
+
+extension Command {
+ var primitiveCommand: PrimitiveCommand {
+ switch self {
+ case .start:
+ return .start
+ case let .reconnect(nextRelay, _):
+ return .reconnect(nextRelay)
+ case .switchKey:
+ return .switchKey
+ case .stop:
+ return .stop
+ default:
+ return .other
+ }
+ }
+}
diff --git a/ios/PacketTunnelCoreTests/Mocks/BlockedStateErrorMapperStub.swift b/ios/PacketTunnelCoreTests/Mocks/BlockedStateErrorMapperStub.swift
new file mode 100644
index 0000000000..eecfcc692e
--- /dev/null
+++ b/ios/PacketTunnelCoreTests/Mocks/BlockedStateErrorMapperStub.swift
@@ -0,0 +1,30 @@
+//
+// BlockedStateErrorMapperStub.swift
+// PacketTunnelCoreTests
+//
+// Created by pronebird on 18/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import PacketTunnelCore
+
+/// Blocked state error mapper stub that can be configured with a block to simulate a desired behavior.
+class BlockedStateErrorMapperStub: BlockedStateErrorMapperProtocol {
+ let block: (Error) -> BlockedStateReason
+
+ /// Initialize a stub that always returns .unknown block reason.
+ init() {
+ self.block = { _ in .unknown }
+ }
+
+ /// Initialize a stub with custom error mapper block.
+ init(block: @escaping (Error) -> BlockedStateReason) {
+ self.block = block
+ }
+
+ func mapError(_ error: Error) -> BlockedStateReason {
+ return block(error)
+ }
+}
diff --git a/ios/PacketTunnelCoreTests/Mocks/MockDefaultPathObserver.swift b/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift
index 2e6c4feb6e..8031ea888c 100644
--- a/ios/PacketTunnelCoreTests/Mocks/MockDefaultPathObserver.swift
+++ b/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift
@@ -1,5 +1,5 @@
//
-// MockDefaultPathObserver.swift
+// DefaultPathObserverFake.swift
// PacketTunnelCoreTests
//
// Created by pronebird on 16/08/2023.
@@ -10,17 +10,17 @@ import Foundation
import NetworkExtension
import PacketTunnelCore
-struct MockNetworkPath: NetworkPath {
+struct NetworkPathStub: NetworkPath {
var status: NetworkExtension.NWPathStatus = .satisfied
}
-/// Mock implementation of a default path observer.
-class MockDefaultPathObserver: DefaultPathObserverProtocol {
+/// Default path observer fake that uses in-memory storage to keep current path and provides a method to simulate path change from tests.
+class DefaultPathObserverFake: DefaultPathObserverProtocol {
var defaultPath: NetworkPath? {
return stateLock.withLock { innerPath }
}
- private var innerPath: NetworkPath = MockNetworkPath()
+ private var innerPath: NetworkPath = NetworkPathStub()
private var stateLock = NSLock()
private var defaultPathHandler: ((NetworkPath) -> Void)?
diff --git a/ios/PacketTunnelCoreTests/Mocks/NetworkCounters.swift b/ios/PacketTunnelCoreTests/Mocks/NetworkCounters.swift
index d20a1ef20f..b16c22a655 100644
--- a/ios/PacketTunnelCoreTests/Mocks/NetworkCounters.swift
+++ b/ios/PacketTunnelCoreTests/Mocks/NetworkCounters.swift
@@ -8,7 +8,7 @@
import Foundation
-/// Protocol describing a type capable of receiving and updating network counters.
+/// A type capable of receiving and updating network counters.
protocol NetworkStatsReporting {
/// Increment number of bytes sent.
func reportBytesSent(_ byteCount: UInt64)
@@ -17,7 +17,7 @@ protocol NetworkStatsReporting {
func reportBytesReceived(_ byteCount: UInt64)
}
-/// Protocol describing a type providing network statistics.
+/// A type providing network statistics.
protocol NetworkStatsProviding {
/// Returns number of bytes sent.
var bytesSent: UInt64 { get }
diff --git a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift
new file mode 100644
index 0000000000..418f691f46
--- /dev/null
+++ b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift
@@ -0,0 +1,40 @@
+//
+// PacketTunnelActor+Mocks.swift
+// PacketTunnelCoreTests
+//
+// Created by pronebird on 25/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import PacketTunnelCore
+
+extension PacketTunnelActorTimings {
+ static var timingsForTests: PacketTunnelActorTimings {
+ return PacketTunnelActorTimings(
+ bootRecoveryPeriodicity: .milliseconds(10),
+ wgKeyPropagationDelay: .milliseconds(10)
+ )
+ }
+}
+
+extension PacketTunnelActor {
+ static func mock(
+ tunnelAdapter: TunnelAdapterProtocol = TunnelAdapterDummy(),
+ tunnelMonitor: TunnelMonitorProtocol = TunnelMonitorStub.nonFallible(),
+ defaultPathObserver: DefaultPathObserverProtocol = DefaultPathObserverFake(),
+ blockedStateErrorMapper: BlockedStateErrorMapperProtocol = BlockedStateErrorMapperStub(),
+ relaySelector: RelaySelectorProtocol = RelaySelectorStub.nonFallible(),
+ settingsReader: SettingsReaderProtocol = SettingsReaderStub.staticConfiguration()
+ ) -> PacketTunnelActor {
+ return PacketTunnelActor(
+ timings: .timingsForTests,
+ tunnelAdapter: tunnelAdapter,
+ tunnelMonitor: tunnelMonitor,
+ defaultPathObserver: defaultPathObserver,
+ blockedStateErrorMapper: blockedStateErrorMapper,
+ relaySelector: relaySelector,
+ settingsReader: settingsReader
+ )
+ }
+}
diff --git a/ios/PacketTunnelCoreTests/Mocks/MockPinger.swift b/ios/PacketTunnelCoreTests/Mocks/PingerMock.swift
index 1ff173b886..9ed50fcd3c 100644
--- a/ios/PacketTunnelCoreTests/Mocks/MockPinger.swift
+++ b/ios/PacketTunnelCoreTests/Mocks/PingerMock.swift
@@ -1,5 +1,5 @@
//
-// MockPinger.swift
+// PingerMock.swift
// PacketTunnelCoreTests
//
// Created by pronebird on 16/08/2023.
@@ -11,8 +11,8 @@ import MullvadTypes
import Network
@testable import PacketTunnelCore
-/// Ping client mock implementation that can be used to simulate network transmission errors and delays.
-class MockPinger: PingerProtocol {
+/// Ping client mock that can be used to simulate network transmission errors and delays.
+class PingerMock: PingerProtocol {
typealias OutcomeDecider = (IPv4Address, UInt16) -> Outcome
private let decideOutcome: OutcomeDecider
diff --git a/ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift b/ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift
new file mode 100644
index 0000000000..64f0c7472a
--- /dev/null
+++ b/ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift
@@ -0,0 +1,57 @@
+//
+// RelaySelectorStub.swift
+// PacketTunnelCoreTests
+//
+// Created by pronebird on 05/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+@testable import MullvadREST
+import MullvadTypes
+import PacketTunnelCore
+@testable import RelaySelector
+import class WireGuardKitTypes.PrivateKey
+
+/// Relay selector stub that accepts a block that can be used to provide custom implementation.
+struct RelaySelectorStub: RelaySelectorProtocol {
+ let block: (RelayConstraints, UInt) throws -> RelaySelectorResult
+
+ func selectRelay(
+ with constraints: RelayConstraints,
+ connectionAttemptFailureCount: UInt
+ ) throws -> RelaySelectorResult {
+ return try block(constraints, connectionAttemptFailureCount)
+ }
+}
+
+extension RelaySelectorStub {
+ /// Returns a relay selector that never fails.
+ static func nonFallible() -> RelaySelectorStub {
+ let publicKey = PrivateKey().publicKey.rawValue
+
+ return RelaySelectorStub { _, _ in
+ return RelaySelectorResult(
+ endpoint: MullvadEndpoint(
+ ipv4Relay: IPv4Endpoint(ip: .loopback, port: 1300),
+ ipv4Gateway: .loopback,
+ ipv6Gateway: .loopback,
+ publicKey: publicKey
+ ),
+ relay: REST.ServerRelay(
+ hostname: "se-got",
+ active: true,
+ owned: true,
+ location: "se-got",
+ provider: "",
+ weight: 0,
+ ipv4AddrIn: .loopback,
+ ipv6AddrIn: .loopback,
+ publicKey: publicKey,
+ includeInCountry: true
+ ),
+ location: Location(country: "", countryCode: "se", city: "", cityCode: "got", latitude: 0, longitude: 0)
+ )
+ }
+ }
+}
diff --git a/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift b/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift
new file mode 100644
index 0000000000..7679b11ac8
--- /dev/null
+++ b/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift
@@ -0,0 +1,38 @@
+//
+// SettingsReaderStub.swift
+// PacketTunnelCoreTests
+//
+// Created by pronebird on 05/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import PacketTunnelCore
+import struct WireGuardKitTypes.IPAddressRange
+import class WireGuardKitTypes.PrivateKey
+
+/// Settings reader stub that can be configured with a block to provide the desired behavior when testing.
+struct SettingsReaderStub: SettingsReaderProtocol {
+ let block: () throws -> Settings
+
+ func read() throws -> Settings {
+ return try block()
+ }
+}
+
+extension SettingsReaderStub {
+ /// Initialize non-fallible settings reader stub that will always return the same static configuration generated at the time of creation.
+ static func staticConfiguration() -> SettingsReaderStub {
+ let staticSettings = Settings(
+ privateKey: PrivateKey(),
+ interfaceAddresses: [IPAddressRange(from: "127.0.0.1/32")!],
+ relayConstraints: RelayConstraints(),
+ dnsServers: .gateway
+ )
+
+ return SettingsReaderStub {
+ return staticSettings
+ }
+ }
+}
diff --git a/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift b/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift
new file mode 100644
index 0000000000..4b7040f756
--- /dev/null
+++ b/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift
@@ -0,0 +1,19 @@
+//
+// TunnelAdapterDummy.swift
+// PacketTunnelCoreTests
+//
+// Created by pronebird on 05/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import PacketTunnelCore
+
+/// Dummy tunnel adapter that does nothing and reports no errors.
+class TunnelAdapterDummy: TunnelAdapterProtocol {
+ func start(configuration: TunnelAdapterConfiguration) async throws {}
+
+ func stop() async throws {}
+
+ func update(configuration: TunnelAdapterConfiguration) async throws {}
+}
diff --git a/ios/PacketTunnelCoreTests/Mocks/MockTunnelDeviceInfo.swift b/ios/PacketTunnelCoreTests/Mocks/TunnelDeviceInfoStub.swift
index bb61dfb443..8dbb90b6fc 100644
--- a/ios/PacketTunnelCoreTests/Mocks/MockTunnelDeviceInfo.swift
+++ b/ios/PacketTunnelCoreTests/Mocks/TunnelDeviceInfoStub.swift
@@ -1,5 +1,5 @@
//
-// MockTunnelDeviceInfo.swift
+// TunnelDeviceInfoStub.swift
// PacketTunnelCoreTests
//
// Created by pronebird on 16/08/2023.
@@ -9,8 +9,8 @@
import Foundation
import PacketTunnelCore
-/// Mock implementation of a tunnel device.
-struct MockTunnelDeviceInfo: TunnelDeviceInfoProtocol {
+/// Tunnel device stub that returns fixed interface name and feeds network stats from the type implementing `NetworkStatsProviding`
+struct TunnelDeviceInfoStub: TunnelDeviceInfoProtocol {
let networkStatsProviding: NetworkStatsProviding
var interfaceName: String? {
diff --git a/ios/PacketTunnelCoreTests/Mocks/TunnelMonitorStub.swift b/ios/PacketTunnelCoreTests/Mocks/TunnelMonitorStub.swift
new file mode 100644
index 0000000000..3b045540b2
--- /dev/null
+++ b/ios/PacketTunnelCoreTests/Mocks/TunnelMonitorStub.swift
@@ -0,0 +1,94 @@
+//
+// TunnelMonitorStub.swift
+// PacketTunnelCoreTests
+//
+// Created by pronebird on 05/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Network
+import PacketTunnelCore
+
+/// Tunnel monitor stub that can be configured with block handler to simulate a specific behavior.
+class TunnelMonitorStub: TunnelMonitorProtocol {
+ enum Command {
+ case start, stop
+ }
+
+ class Dispatcher {
+ typealias BlockHandler = (TunnelMonitorEvent, DispatchTimeInterval) -> Void
+
+ private let block: BlockHandler
+ init(_ block: @escaping BlockHandler) {
+ self.block = block
+ }
+
+ func send(_ event: TunnelMonitorEvent, after delay: DispatchTimeInterval = .never) {
+ block(event, delay)
+ }
+ }
+
+ typealias EventHandler = (TunnelMonitorEvent) -> Void
+ typealias SimulationHandler = (Command, Dispatcher) -> Void
+
+ private let stateLock = NSLock()
+
+ var onEvent: EventHandler? {
+ get {
+ stateLock.withLock { _onEvent }
+ }
+ set {
+ stateLock.withLock {
+ _onEvent = newValue
+ }
+ }
+ }
+
+ private var _onEvent: EventHandler?
+ private let simulationBlock: SimulationHandler
+
+ init(_ simulationBlock: @escaping SimulationHandler) {
+ self.simulationBlock = simulationBlock
+ }
+
+ func start(probeAddress: IPv4Address) {
+ sendCommand(.start)
+ }
+
+ func stop() {
+ sendCommand(.stop)
+ }
+
+ func onWake() {}
+
+ func onSleep() {}
+
+ private func dispatch(_ event: TunnelMonitorEvent, after delay: DispatchTimeInterval = .never) {
+ if case .never = delay {
+ onEvent?(event)
+ } else {
+ DispatchQueue.main.asyncAfter(wallDeadline: .now() + delay) { [weak self] in
+ self?.onEvent?(event)
+ }
+ }
+ }
+
+ private func sendCommand(_ command: Command) {
+ let dispatcher = Dispatcher { [weak self] event, delay in
+ self?.dispatch(event, after: delay)
+ }
+ simulationBlock(command, dispatcher)
+ }
+}
+
+extension TunnelMonitorStub {
+ /// Returns a mock of tunnel monitor that always reports that connection is established after a short delay.
+ static func nonFallible() -> TunnelMonitorStub {
+ TunnelMonitorStub { command, dispatcher in
+ if case .start = command {
+ dispatcher.send(.connectionEstablished, after: .milliseconds(10))
+ }
+ }
+ }
+}
diff --git a/ios/PacketTunnelCoreTests/TaskSleepTests.swift b/ios/PacketTunnelCoreTests/TaskSleepTests.swift
new file mode 100644
index 0000000000..ad2cb7a361
--- /dev/null
+++ b/ios/PacketTunnelCoreTests/TaskSleepTests.swift
@@ -0,0 +1,28 @@
+//
+// TaskSleepTests.swift
+// PacketTunnelCoreTests
+//
+// Created by pronebird on 25/09/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+@testable import PacketTunnelCore
+import XCTest
+
+final class TaskSleepTests: XCTestCase {
+ func testCancellation() async throws {
+ let task = Task {
+ try await Task.sleepUsingContinuousClock(for: .seconds(1))
+ }
+
+ task.cancel()
+
+ do {
+ try await task.value
+
+ XCTFail("Task must be cancelled.")
+ } catch {
+ XCTAssert(error is CancellationError)
+ }
+ }
+}
diff --git a/ios/PacketTunnelCoreTests/TunnelMonitorTests.swift b/ios/PacketTunnelCoreTests/TunnelMonitorTests.swift
index a2d2ac8a24..b742ea117f 100644
--- a/ios/PacketTunnelCoreTests/TunnelMonitorTests.swift
+++ b/ios/PacketTunnelCoreTests/TunnelMonitorTests.swift
@@ -19,7 +19,7 @@ final class TunnelMonitorTests: XCTestCase {
let connectionLostExpectation = expectation(description: "Should not report connection loss")
connectionLostExpectation.isInverted = true
- let pinger = MockPinger(networkStatsReporting: networkCounters) { _, _ in
+ let pinger = PingerMock(networkStatsReporting: networkCounters) { _, _ in
return .sendReply()
}
@@ -32,9 +32,6 @@ final class TunnelMonitorTests: XCTestCase {
case .connectionLost:
connectionLostExpectation.fulfill()
-
- case .networkReachabilityChanged:
- break
}
}
@@ -45,7 +42,7 @@ final class TunnelMonitorTests: XCTestCase {
func testInitialConnectionTimings() {
// Setup pinger so that it never receives any replies.
- let pinger = MockPinger(networkStatsReporting: networkCounters) { _, _ in .ignore }
+ let pinger = PingerMock(networkStatsReporting: networkCounters) { _, _ in .ignore }
let timings = TunnelMonitorTimings(
pingTimeout: .milliseconds(300),
@@ -104,9 +101,6 @@ final class TunnelMonitorTests: XCTestCase {
case .connectionEstablished:
XCTFail("Connection should fail.")
-
- case .networkReachabilityChanged:
- break
}
}
@@ -122,8 +116,8 @@ extension TunnelMonitorTests {
return TunnelMonitor(
eventQueue: .main,
pinger: pinger,
- tunnelDeviceInfo: MockTunnelDeviceInfo(networkStatsProviding: networkCounters),
- defaultPathObserver: MockDefaultPathObserver(),
+ tunnelDeviceInfo: TunnelDeviceInfoStub(networkStatsProviding: networkCounters),
+ defaultPathObserver: DefaultPathObserverFake(),
timings: timings
)
}
diff --git a/ios/RelaySelector/RelaySelector.swift b/ios/RelaySelector/RelaySelector.swift
index c5ddef650e..bb25c1c960 100644
--- a/ios/RelaySelector/RelaySelector.swift
+++ b/ios/RelaySelector/RelaySelector.swift
@@ -276,7 +276,7 @@ public struct NoRelaysSatisfyingConstraintsError: LocalizedError {
}
}
-public struct RelaySelectorResult: Codable {
+public struct RelaySelectorResult: Codable, Equatable {
public var endpoint: MullvadEndpoint
public var relay: REST.ServerRelay
public var location: Location