From f29c4f82dfe6c98dc90b639e62bcb745db434cd9 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Fri, 22 Sep 2023 12:13:06 +0200 Subject: [PATCH] PacketTunnel: introduce proper state and blocked state --- ios/MullvadVPN.xcodeproj/project.pbxproj | 336 ++++++-- .../Coordinators/ApplicationCoordinator.swift | 2 +- .../TunnelStatusNotificationProvider.swift | 10 +- .../MapConnectionStatusOperation.swift | 12 +- .../TunnelManager/StopTunnelOperation.swift | 2 +- .../TunnelManager/TunnelManager.swift | 61 +- .../TunnelManager/TunnelState.swift | 9 +- .../Tunnel/TunnelControlView.swift | 18 +- .../Tunnel/TunnelViewController.swift | 2 +- .../DeviceCheck/DeviceCheck.swift | 70 ++ .../DeviceCheck/DeviceCheckOperation.swift | 2 +- .../DeviceCheckRemoteService.swift | 2 +- .../MullvadEndpoint+WgEndpoint.swift | 23 - .../PacketTunnelConfiguration.swift | 74 -- ios/PacketTunnel/PacketTunnelProvider.swift | 786 ------------------ .../AppMessageHandler.swift | 82 ++ .../BlockedStateErrorMapper.swift | 65 ++ .../DeviceCheck+BlockedStateReason.swift | 25 + .../PacketTunnelProvider/DeviceChecker.swift | 57 ++ .../NEProviderStopReason+Debug.swift | 0 .../PacketTunnelPathObserver.swift | 0 .../PacketTunnelProvider.swift | 266 ++++++ .../RelaySelectorWrapper.swift | 28 + .../PacketTunnelProvider/SettingsReader.swift | 75 ++ .../State+Extensions.swift | 57 ++ ios/PacketTunnel/WgAdapterDeviceInfo.swift | 85 -- .../WireGuardAdapter/WgAdapter.swift | 151 ++++ .../WireGuardAdapter+Async.swift | 48 ++ .../WireGuardAdapterError+Localization.swift | 0 .../WireGuardLogLevel+Logging.swift | 0 .../Actor/Actor+ConnectionMonitoring.swift | 74 ++ .../Actor/Actor+ErrorState.swift | 148 ++++ .../Actor/Actor+Extensions.swift | 54 ++ .../Actor/Actor+KeyPolicy.swift | 175 ++++ .../Actor/Actor+NetworkReachability.swift | 79 ++ ios/PacketTunnelCore/Actor/Actor+Public.swift | 59 ++ .../Actor/Actor+SleepCycle.swift | 29 + ios/PacketTunnelCore/Actor/Actor.swift | 357 ++++++++ ios/PacketTunnelCore/Actor/AnyTask.swift | 17 + .../Actor/AutoCancellingTask.swift | 26 + ios/PacketTunnelCore/Actor/Command.swift | 72 ++ .../Actor/CommandChannel.swift | 219 +++++ .../Actor/ConfigurationBuilder.swift | 54 ++ .../NetworkPath+NetworkReachability.swift | 28 + .../BlockedStateErrorMapperProtocol.swift | 15 + .../Protocols/RelaySelectorProtocol.swift | 17 + .../Protocols/SettingsReaderProtocol.swift | 60 ++ .../Protocols/TunnelAdapterProtocol.swift | 38 + .../Actor}/StartOptions.swift | 21 +- .../Actor/State+Extensions.swift | 112 +++ ios/PacketTunnelCore/Actor/State.swift | 215 +++++ .../Actor/Task+Duration.swift | 56 ++ ios/PacketTunnelCore/Actor/Timings.swift | 28 + .../IPC/PacketTunnelErrorWrapper.swift | 49 -- .../IPC/PacketTunnelStatus.swift | 87 +- ios/PacketTunnelCore/Pinger/Pinger.swift | 1 + .../Pinger/PingerProtocol.swift | 9 + .../DefaultPathObserverProtocol.swift | 3 + .../TunnelDeviceInfoProtocol.swift | 1 + .../TunnelMonitor/TunnelMonitor.swift | 11 +- .../TunnelMonitor/TunnelMonitorProtocol.swift | 6 +- .../URLRequestProxy/URLRequestProxy.swift | 24 +- ios/PacketTunnelCoreTests/ActorTests.swift | 102 +++ .../CommandChannelTests.swift | 109 +++ .../Mocks/BlockedStateErrorMapperStub.swift | 30 + ...er.swift => DefaultPathObserverFake.swift} | 10 +- .../Mocks/NetworkCounters.swift | 4 +- .../Mocks/PacketTunnelActor+Mocks.swift | 40 + .../{MockPinger.swift => PingerMock.swift} | 6 +- .../Mocks/RelaySelectorStub.swift | 57 ++ .../Mocks/SettingsReaderStub.swift | 38 + .../Mocks/TunnelAdapterDummy.swift | 19 + ...eInfo.swift => TunnelDeviceInfoStub.swift} | 6 +- .../Mocks/TunnelMonitorStub.swift | 94 +++ .../TaskSleepTests.swift | 28 + .../TunnelMonitorTests.swift | 14 +- ios/RelaySelector/RelaySelector.swift | 2 +- 77 files changed, 3766 insertions(+), 1285 deletions(-) create mode 100644 ios/PacketTunnel/DeviceCheck/DeviceCheck.swift delete mode 100644 ios/PacketTunnel/MullvadEndpoint+WgEndpoint.swift delete mode 100644 ios/PacketTunnel/PacketTunnelConfiguration.swift delete mode 100644 ios/PacketTunnel/PacketTunnelProvider.swift create mode 100644 ios/PacketTunnel/PacketTunnelProvider/AppMessageHandler.swift create mode 100644 ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift create mode 100644 ios/PacketTunnel/PacketTunnelProvider/DeviceCheck+BlockedStateReason.swift create mode 100644 ios/PacketTunnel/PacketTunnelProvider/DeviceChecker.swift rename ios/PacketTunnel/{ => PacketTunnelProvider}/NEProviderStopReason+Debug.swift (100%) rename ios/PacketTunnel/{ => PacketTunnelProvider}/PacketTunnelPathObserver.swift (100%) create mode 100644 ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift create mode 100644 ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift create mode 100644 ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift create mode 100644 ios/PacketTunnel/PacketTunnelProvider/State+Extensions.swift delete mode 100644 ios/PacketTunnel/WgAdapterDeviceInfo.swift create mode 100644 ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift create mode 100644 ios/PacketTunnel/WireGuardAdapter/WireGuardAdapter+Async.swift rename ios/PacketTunnel/{ => WireGuardAdapter}/WireGuardAdapterError+Localization.swift (100%) rename ios/PacketTunnel/{ => WireGuardAdapter}/WireGuardLogLevel+Logging.swift (100%) create mode 100644 ios/PacketTunnelCore/Actor/Actor+ConnectionMonitoring.swift create mode 100644 ios/PacketTunnelCore/Actor/Actor+ErrorState.swift create mode 100644 ios/PacketTunnelCore/Actor/Actor+Extensions.swift create mode 100644 ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift create mode 100644 ios/PacketTunnelCore/Actor/Actor+NetworkReachability.swift create mode 100644 ios/PacketTunnelCore/Actor/Actor+Public.swift create mode 100644 ios/PacketTunnelCore/Actor/Actor+SleepCycle.swift create mode 100644 ios/PacketTunnelCore/Actor/Actor.swift create mode 100644 ios/PacketTunnelCore/Actor/AnyTask.swift create mode 100644 ios/PacketTunnelCore/Actor/AutoCancellingTask.swift create mode 100644 ios/PacketTunnelCore/Actor/Command.swift create mode 100644 ios/PacketTunnelCore/Actor/CommandChannel.swift create mode 100644 ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift create mode 100644 ios/PacketTunnelCore/Actor/NetworkPath+NetworkReachability.swift create mode 100644 ios/PacketTunnelCore/Actor/Protocols/BlockedStateErrorMapperProtocol.swift create mode 100644 ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift create mode 100644 ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift create mode 100644 ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift rename ios/{PacketTunnel => PacketTunnelCore/Actor}/StartOptions.swift (65%) create mode 100644 ios/PacketTunnelCore/Actor/State+Extensions.swift create mode 100644 ios/PacketTunnelCore/Actor/State.swift create mode 100644 ios/PacketTunnelCore/Actor/Task+Duration.swift create mode 100644 ios/PacketTunnelCore/Actor/Timings.swift delete mode 100644 ios/PacketTunnelCore/IPC/PacketTunnelErrorWrapper.swift create mode 100644 ios/PacketTunnelCoreTests/ActorTests.swift create mode 100644 ios/PacketTunnelCoreTests/CommandChannelTests.swift create mode 100644 ios/PacketTunnelCoreTests/Mocks/BlockedStateErrorMapperStub.swift rename ios/PacketTunnelCoreTests/Mocks/{MockDefaultPathObserver.swift => DefaultPathObserverFake.swift} (74%) create mode 100644 ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift rename ios/PacketTunnelCoreTests/Mocks/{MockPinger.swift => PingerMock.swift} (95%) create mode 100644 ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift create mode 100644 ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift create mode 100644 ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift rename ios/PacketTunnelCoreTests/Mocks/{MockTunnelDeviceInfo.swift => TunnelDeviceInfoStub.swift} (69%) create mode 100644 ios/PacketTunnelCoreTests/Mocks/TunnelMonitorStub.swift create mode 100644 ios/PacketTunnelCoreTests/TaskSleepTests.swift diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index d172d39fe360..7a8db6966edd 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 = ""; }; 06410E03292D0F7100AFC18C /* SettingsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsParser.swift; sourceTree = ""; }; 06410E06292D108E00AFC18C /* SettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStore.swift; sourceTree = ""; }; - 06410E172934F43B00AFC18C /* PacketTunnelErrorWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelErrorWrapper.swift; sourceTree = ""; }; 06799AB428F98CE700ACD94E /* le_root_cert.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = le_root_cert.cer; sourceTree = ""; }; 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 = ""; }; @@ -1043,6 +1077,10 @@ 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokedDeviceViewController.swift; sourceTree = ""; }; 58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTests.swift; sourceTree = ""; }; 580CBFB72848D503007878F0 /* OperationConditionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationConditionTests.swift; sourceTree = ""; }; + 580D6B892AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkPath+NetworkReachability.swift"; sourceTree = ""; }; + 580D6B8B2AB3369300B2D6E0 /* BlockedStateErrorMapperProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedStateErrorMapperProtocol.swift; sourceTree = ""; }; + 580D6B8D2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedStateErrorMapper.swift; sourceTree = ""; }; + 580D6B912AB360BE00B2D6E0 /* DeviceCheck+BlockedStateReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeviceCheck+BlockedStateReason.swift"; sourceTree = ""; }; 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperation.swift; sourceTree = ""; }; 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV2.swift; sourceTree = ""; }; 580F8B8528197958002E0998 /* DNSSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSSettings.swift; sourceTree = ""; }; @@ -1061,11 +1099,12 @@ 581943E228F8010400B0CB5E /* Date+LogFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+LogFormat.swift"; sourceTree = ""; }; 581943E328F8010400B0CB5E /* CustomFormatLogHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFormatLogHandler.swift; sourceTree = ""; }; 581943E428F8010400B0CB5E /* OSLogHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLogHandler.swift; sourceTree = ""; }; + 5819ABC22A8CF02C007B59A6 /* TunnelAdapterProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelAdapterProtocol.swift; sourceTree = ""; }; 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAddDNSEntryCell.swift; sourceTree = ""; }; 581DA2722A1E227D0046ED47 /* RESTTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTypes.swift; sourceTree = ""; }; 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgKeyRotation.swift; sourceTree = ""; }; - 581F23AC2A8CF92100788AB6 /* MockDefaultPathObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDefaultPathObserver.swift; sourceTree = ""; }; - 581F23AE2A8CF94D00788AB6 /* MockPinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPinger.swift; sourceTree = ""; }; + 581F23AC2A8CF92100788AB6 /* DefaultPathObserverFake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultPathObserverFake.swift; sourceTree = ""; }; + 581F23AE2A8CF94D00788AB6 /* PingerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingerMock.swift; sourceTree = ""; }; 5820675A26E6576800655B05 /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = ""; }; 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerErrors.swift; sourceTree = ""; }; 5820EDA8288FE064006BF4E4 /* DeviceManagementInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagementInteractor.swift; sourceTree = ""; }; @@ -1075,8 +1114,10 @@ 58225D252A84E8A10083D7F1 /* DefaultPathObserverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultPathObserverProtocol.swift; sourceTree = ""; }; 58225D272A84F23B0083D7F1 /* PacketTunnelPathObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelPathObserver.swift; sourceTree = ""; }; 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObserver.swift; sourceTree = ""; }; - 582403142A821FB000163DE8 /* WgAdapterDeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgAdapterDeviceInfo.swift; sourceTree = ""; }; + 5824030C2A811B0000163DE8 /* State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = ""; }; 582403162A821FD700163DE8 /* TunnelDeviceInfoProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDeviceInfoProtocol.swift; sourceTree = ""; }; + 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorProtocol.swift; sourceTree = ""; }; + 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapper.swift; sourceTree = ""; }; 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportViewController.swift; sourceTree = ""; }; 58293FB025124117005D0BB5 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = ""; }; 58293FB2251241B3005D0BB5 /* CustomTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextView.swift; sourceTree = ""; }; @@ -1085,11 +1126,22 @@ 582BB1AE229566420055B6EF /* SettingsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = ""; }; 582BB1B0229569620055B6EF /* UINavigationBar+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Appearance.swift"; sourceTree = ""; }; 582FFA82290A84E700895745 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 58342C032AAB61FB003BA12D /* State+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "State+Extensions.swift"; sourceTree = ""; }; 5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = ""; }; 5838318A27C40A3900000571 /* Pinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pinger.swift; sourceTree = ""; }; + 5838321A2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+Mocks.swift"; sourceTree = ""; }; + 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskSleepTests.swift; sourceTree = ""; }; + 5838321E2AC3160A00EA2071 /* Actor+KeyPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+KeyPolicy.swift"; sourceTree = ""; }; + 583832202AC3174700EA2071 /* Actor+NetworkReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+NetworkReachability.swift"; sourceTree = ""; }; + 583832222AC3181400EA2071 /* Actor+ErrorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+ErrorState.swift"; sourceTree = ""; }; + 583832242AC318A100EA2071 /* Actor+ConnectionMonitoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+ConnectionMonitoring.swift"; sourceTree = ""; }; + 583832262AC3193600EA2071 /* Actor+SleepCycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+SleepCycle.swift"; sourceTree = ""; }; + 583832282AC3DF1300EA2071 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = ""; }; + 5838322A2AC3EF9600EA2071 /* CommandChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandChannel.swift; sourceTree = ""; }; 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStateAccessor.swift; sourceTree = ""; }; 583DA21325FA4B5C00318683 /* LocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSource.swift; sourceTree = ""; }; 583E1E292848DF67004838B3 /* OperationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationObserverTests.swift; sourceTree = ""; }; + 583E60952A9F6D0800DC61EF /* ConfigurationBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationBuilder.swift; sourceTree = ""; }; 583FE00B29C0C7FD006E85F9 /* ModalPresentationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalPresentationConfiguration.swift; sourceTree = ""; }; 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSplitViewController.swift; sourceTree = ""; }; 583FE01129C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationControllerDismissalInterceptor.swift; sourceTree = ""; }; @@ -1119,6 +1171,7 @@ 585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPUnsafeListener.swift; sourceTree = ""; }; 585A02EA2A4B285800C6CAFF /* UDPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPConnection.swift; sourceTree = ""; }; 585A02EC2A4B28F300C6CAFF /* TCPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPConnection.swift; sourceTree = ""; }; + 585B1FF12AB0BC69008AD470 /* State+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "State+Extensions.swift"; sourceTree = ""; }; 585CA70E25F8C44600B47C62 /* UIMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMetrics.swift; sourceTree = ""; }; 585DA87626B024A600B8C587 /* CachedRelays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedRelays.swift; sourceTree = ""; }; 585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelProviderMessage.swift; sourceTree = ""; }; @@ -1132,6 +1185,7 @@ 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCellFactory.swift; sourceTree = ""; }; 5864AF0129C7879B005B0CD9 /* CellFactoryProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellFactoryProtocol.swift; sourceTree = ""; }; 5864AF0229C7879B005B0CD9 /* PreferencesCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesCellFactory.swift; sourceTree = ""; }; + 5864AF7C2A9F4DC9008BC928 /* SettingsReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsReader.swift; sourceTree = ""; }; 5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MullvadVPN.entitlements; sourceTree = ""; }; 5867770D29096984006F721F /* OutOfTimeInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutOfTimeInteractor.swift; sourceTree = ""; }; 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInteractorFactory.swift; sourceTree = ""; }; @@ -1143,7 +1197,11 @@ 58695A9F2A4ADA9200328DB3 /* TunnelObfuscationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscationTests.swift; sourceTree = ""; }; 586A95112901321B007BAF2B /* IPv6Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv6Endpoint.swift; sourceTree = ""; }; 586A951329013235007BAF2B /* AnyIPEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPEndpoint.swift; sourceTree = ""; }; + 586C14572AC463BB00245C01 /* CommandChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandChannelTests.swift; sourceTree = ""; }; + 586C14592AC4735F00245C01 /* Actor+Public.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+Public.swift"; sourceTree = ""; }; 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTunnelProviderMessageOperation.swift; sourceTree = ""; }; + 586E7A2C2A987689006DAB1B /* SettingsReaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsReaderProtocol.swift; sourceTree = ""; }; + 586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Duration.swift"; sourceTree = ""; }; 586F2BE129F6916F009E6924 /* shadowsocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = shadowsocks.h; path = "shadowsocks-proxy/include/shadowsocks.h"; sourceTree = ""; }; 5871167E2910035700D41AAC /* PreferencesInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesInteractor.swift; sourceTree = ""; }; 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLog.swift; sourceTree = ""; }; @@ -1181,6 +1239,7 @@ 587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CharacterSet+IPAddress.swift"; sourceTree = ""; }; 587EB671271451E300123C75 /* PreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewModel.swift; sourceTree = ""; }; 587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDataSourceDelegate.swift; sourceTree = ""; }; + 5883955F2A9DEEA1008B63F6 /* WgAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgAdapter.swift; sourceTree = ""; }; 588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadTunnelConfigurationOperation.swift; sourceTree = ""; }; 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetAccountOperation.swift; sourceTree = ""; }; 5888AD82227B11080051EB06 /* SelectLocationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationCell.swift; sourceTree = ""; }; @@ -1249,6 +1308,7 @@ 58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyTransport.swift; sourceTree = ""; }; 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeServerProxy.swift; sourceTree = ""; }; 58BDEB9C2A98F69E00F578F2 /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = ""; }; + 58BDEBA02A9CA14B00F578F2 /* AnyTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyTask.swift; sourceTree = ""; }; 58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTracker.swift; sourceTree = ""; }; 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationConfiguration.swift; sourceTree = ""; }; 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputGroupView.swift; sourceTree = ""; }; @@ -1280,7 +1340,6 @@ 58CE5E6A224146210008646E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 58CE5E6F224146210008646E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 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 = ""; }; 58CE5E7D224146470008646E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 58CE5E7E224146470008646E /* PacketTunnel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PacketTunnel.entitlements; sourceTree = ""; }; 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 = ""; }; 58D229B6298D1D5200BB5A2D /* URLRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxy.swift; sourceTree = ""; }; + 58DDA18E2ABC32380039C360 /* Timings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timings.swift; sourceTree = ""; }; 58DF28A42417CB4B00E836B0 /* StorePaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentManager.swift; sourceTree = ""; }; 58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationSmokeTests.swift; sourceTree = ""; }; 58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireGuardAdapterError+Localization.swift"; sourceTree = ""; }; - 58E0729C28814AAE008902F8 /* PacketTunnelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelConfiguration.swift; sourceTree = ""; }; 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireGuardLogLevel+Logging.swift"; sourceTree = ""; }; - 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MullvadEndpoint+WgEndpoint.swift"; sourceTree = ""; }; 58E0A98727C8F46300FE6BDD /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = ""; }; 58E0E2832A3718CE002E3420 /* URLSessionShadowsocksTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionShadowsocksTransport.swift; sourceTree = ""; }; 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationUIHandler.swift; sourceTree = ""; }; @@ -1311,7 +1369,9 @@ 58E511EA28DDE18400B0BCDE /* Error+Chain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Chain.swift"; sourceTree = ""; }; 58E7BA182A975DF70068EC3A /* RESTTransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportProvider.swift; sourceTree = ""; }; 58E973DD24850EB600096F90 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = ""; }; - 58EC06792A8D208D00BEB973 /* MockTunnelDeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnelDeviceInfo.swift; sourceTree = ""; }; + 58E9C3832A4EF15300CFDEAC /* WireGuardAdapter+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireGuardAdapter+Async.swift"; sourceTree = ""; }; + 58E9C3852A4EF1CB00CFDEAC /* Actor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actor.swift; sourceTree = ""; }; + 58EC06792A8D208D00BEB973 /* TunnelDeviceInfoStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDeviceInfoStub.swift; sourceTree = ""; }; 58EC067B2A8D2A0B00BEB973 /* NetworkCounters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkCounters.swift; sourceTree = ""; }; 58ECD29123F178FD004298B6 /* Screenshots.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Screenshots.xcconfig; sourceTree = ""; }; 58ED3A132A7C199C0085CE65 /* StartOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartOptions.swift; sourceTree = ""; }; @@ -1327,6 +1387,10 @@ 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotateKeyOperation.swift; sourceTree = ""; }; 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarView.swift; sourceTree = ""; }; 58F3C0A524A50155003E76BE /* relays.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = relays.json; sourceTree = ""; }; + 58F3F3652AA086A400D3B0A4 /* AutoCancellingTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCancellingTask.swift; sourceTree = ""; }; + 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; + 58F7753C2AB8473200425B47 /* BlockedStateErrorMapperStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedStateErrorMapperStub.swift; sourceTree = ""; }; + 58F775422AB9E3EF00425B47 /* AppMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMessageHandler.swift; sourceTree = ""; }; 58F7D26427EB50A300E4D821 /* ResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultOperation.swift; sourceTree = ""; }; 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportReviewViewController.swift; sourceTree = ""; }; 58FB865426E8BF3100F188BC /* StorePaymentManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentManagerError.swift; sourceTree = ""; }; @@ -1338,7 +1402,15 @@ 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Formatting.swift"; sourceTree = ""; }; 58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseButton.swift; sourceTree = ""; }; 58FDF2D82A0BA11900C2B061 /* DeviceCheckOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceCheckOperation.swift; sourceTree = ""; }; + 58FE25D32AA729B5003D1918 /* ActorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActorTests.swift; sourceTree = ""; }; + 58FE25EB2AA77638003D1918 /* TunnelMonitorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorStub.swift; sourceTree = ""; }; + 58FE25ED2AA7764E003D1918 /* TunnelAdapterDummy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelAdapterDummy.swift; sourceTree = ""; }; + 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorStub.swift; sourceTree = ""; }; + 58FE25F12AA77674003D1918 /* SettingsReaderStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsReaderStub.swift; sourceTree = ""; }; + 58FE25F32AA9D730003D1918 /* Actor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+Extensions.swift"; sourceTree = ""; }; + 58FE65922AB1CDE000E53CB5 /* DeviceCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCheck.swift; sourceTree = ""; }; 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticKeyboardResponder.swift; sourceTree = ""; }; + 58FF23A22AB09BEE003A2AF2 /* DeviceChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceChecker.swift; sourceTree = ""; }; 58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = ""; }; 7A02D4EA2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNScreenshots.xctestplan; sourceTree = ""; }; 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = ""; }; @@ -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 = ""; }; + 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 = ""; + }; 58695A9E2A4ADA9200328DB3 /* TunnelObfuscationTests */ = { isa = PBXGroup; children = ( @@ -2157,9 +2257,21 @@ path = "Notification Providers"; sourceTree = ""; }; + 588395612A9DF497008B63F6 /* WireGuardAdapter */ = { + isa = PBXGroup; + children = ( + 5883955F2A9DEEA1008B63F6 /* WgAdapter.swift */, + 58E9C3832A4EF15300CFDEAC /* WireGuardAdapter+Async.swift */, + 58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */, + 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */, + ); + path = WireGuardAdapter; + sourceTree = ""; + }; 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 = ""; @@ -2570,13 +2678,30 @@ path = TunnelMonitor; sourceTree = ""; }; + 58E7A0312AA0715100C57861 /* Protocols */ = { + isa = PBXGroup; + children = ( + 5819ABC22A8CF02C007B59A6 /* TunnelAdapterProtocol.swift */, + 586E7A2C2A987689006DAB1B /* SettingsReaderProtocol.swift */, + 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */, + 580D6B8B2AB3369300B2D6E0 /* BlockedStateErrorMapperProtocol.swift */, + ); + path = Protocols; + sourceTree = ""; + }; 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 = ""; @@ -2602,6 +2727,23 @@ path = Assets; sourceTree = ""; }; + 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 = ""; + }; 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 94ae7ef1dc73..12c4e00478ee 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 021afc95ce83..87164e0eaa51 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 f7d8346d597d..78541be833b7 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 fc52a5872aca..7bcce98a734a 100644 --- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift @@ -35,7 +35,7 @@ class StopTunnelOperation: ResultOperation { 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 4e6bfc6163f1..9966b40fc796 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 5c60f429c8d4..aed7fb2cc28a 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 2c26c6a07ceb..cf76e00d3e34 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 5a64d50b2a87..10095a0f3a3d 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 000000000000..e84c07a22ebf --- /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 b38b3e958e14..ce6913b5cc17 100644 --- a/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift +++ b/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift @@ -42,7 +42,7 @@ final class DeviceCheckOperation: ResultOperation { 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 da8c11726deb..9ad8bcbc75bf 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) -> 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 5609958b9ef1..000000000000 --- 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 ffece5520776..000000000000 --- 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 910c8b2befa2..000000000000 --- 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 000000000000..952fcb4bdeb7 --- /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(_ 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 000000000000..a31c508288a6 --- /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 000000000000..f6c2d28c1d20 --- /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 000000000000..e4fa756b25c0 --- /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 similarity index 100% rename from ios/PacketTunnel/NEProviderStopReason+Debug.swift rename to ios/PacketTunnel/PacketTunnelProvider/NEProviderStopReason+Debug.swift diff --git a/ios/PacketTunnel/PacketTunnelPathObserver.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelPathObserver.swift similarity index 100% rename from ios/PacketTunnel/PacketTunnelPathObserver.swift rename to ios/PacketTunnel/PacketTunnelProvider/PacketTunnelPathObserver.swift diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift new file mode 100644 index 000000000000..9c4e53c79df4 --- /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 000000000000..31e1e68f3cdc --- /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 000000000000..48874253a568 --- /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 000000000000..99862e152199 --- /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 11fca33b94c8..000000000000 --- 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 000000000000..e373b0e57321 --- /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 000000000000..1644597b9436 --- /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 similarity index 100% rename from ios/PacketTunnel/WireGuardAdapterError+Localization.swift rename to ios/PacketTunnel/WireGuardAdapter/WireGuardAdapterError+Localization.swift diff --git a/ios/PacketTunnel/WireGuardLogLevel+Logging.swift b/ios/PacketTunnel/WireGuardAdapter/WireGuardLogLevel+Logging.swift similarity index 100% rename from ios/PacketTunnel/WireGuardLogLevel+Logging.swift rename to 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 000000000000..2980d4d11d2b --- /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 000000000000..b95e0c8d5e7e --- /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 000000000000..71bd13c057cd --- /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 { + 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 000000000000..0cf8351523c2 --- /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 000000000000..34a4e02ed154 --- /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 000000000000..7a50477c29d5 --- /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 000000000000..c6339ce11ebe --- /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 000000000000..18be2e2d409e --- /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 000000000000..43c44b064414 --- /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 000000000000..c80e28f88017 --- /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 000000000000..43771e62afb4 --- /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 000000000000..ca19794b967f --- /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] = [] + + 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 000000000000..c4a731aa7829 --- /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 000000000000..3ebedfebcf81 --- /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 000000000000..7afb5fc645ee --- /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 000000000000..b856b2f1fe99 --- /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 000000000000..dc3bfbcd0faa --- /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 000000000000..c4c499f83e60 --- /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 similarity index 65% rename from ios/PacketTunnel/StartOptions.swift rename to ios/PacketTunnelCore/Actor/StartOptions.swift index 773c2e343061..d2ebe72611b3 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 000000000000..cfaed6cf0787 --- /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 000000000000..2c5cfd13b81f --- /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 000000000000..32f7a3d8665e --- /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 000000000000..4e2d5d8b7706 --- /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 381b64898af5..000000000000 --- 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 5eece926a6e4..f18f5510374a 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 0d754f3cfade..f5a0cee0b512 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 9df205ab6dad..2f1c9d544cbf 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 97b31a1683aa..d4d192fb743e 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 fcd1ff88c31c..829265c60732 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 576a29533e43..195a8acde882 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 3bee3942425b..bae8250d27a3 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 a5b3be3bc2a8..e6f6b38e1549 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 000000000000..7b8cd6953875 --- /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 000000000000..dc622434b936 --- /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 000000000000..eecfcc692edc --- /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 similarity index 74% rename from ios/PacketTunnelCoreTests/Mocks/MockDefaultPathObserver.swift rename to ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift index 2e6c4feb6e4e..8031ea888c18 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 d20a1ef20f5a..b16c22a6555d 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 000000000000..418f691f46e3 --- /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 similarity index 95% rename from ios/PacketTunnelCoreTests/Mocks/MockPinger.swift rename to ios/PacketTunnelCoreTests/Mocks/PingerMock.swift index 1ff173b8863b..9ed50fcd3ca6 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 000000000000..64f0c7472a3d --- /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 000000000000..7679b11ac84f --- /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 000000000000..4b7040f7566f --- /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 similarity index 69% rename from ios/PacketTunnelCoreTests/Mocks/MockTunnelDeviceInfo.swift rename to ios/PacketTunnelCoreTests/Mocks/TunnelDeviceInfoStub.swift index bb61dfb443c3..8dbb90b6fc83 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 000000000000..3b045540b2e4 --- /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 000000000000..ad2cb7a36128 --- /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 a2d2ac8a24e0..b742ea117f6c 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 c5ddef650e46..bb25c1c960c9 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