From 9840fac6155d36f98002143e34f4d33f601bd6c1 Mon Sep 17 00:00:00 2001 From: Jon Petersson Date: Tue, 26 Sep 2023 16:11:46 +0200 Subject: [PATCH] Add tests for AppMessageHandler --- ios/MullvadVPN.xcodeproj/project.pbxproj | 108 ++--- .../BlockedStateErrorMapper.swift | 4 + .../State+Extensions.swift | 57 --- .../Actor/ConfigurationBuilder.swift | 29 +- ...ketTunnelActor+ConnectionMonitoring.swift} | 0 ...ift => PacketTunnelActor+ErrorState.swift} | 0 ...ift => PacketTunnelActor+Extensions.swift} | 0 ...wift => PacketTunnelActor+KeyPolicy.swift} | 4 +- ...cketTunnelActor+NetworkReachability.swift} | 0 ...c.swift => PacketTunnelActor+Public.swift} | 2 +- ...ift => PacketTunnelActor+SleepCycle.swift} | 0 .../{Actor.swift => PacketTunnelActor.swift} | 2 +- .../Actor/State+Extensions.swift | 47 +- ios/PacketTunnelCore/Actor/State.swift | 11 +- .../IPC}/AppMessageHandler.swift | 9 +- ios/PacketTunnelCoreTests/ActorTests.swift | 102 ---- .../AppMessageHandlerTests.swift | 434 ++++++++++++++++++ .../Mocks/BlockedStateErrorMapperStub.swift | 2 +- .../Mocks/DefaultPathObserverFake.swift | 4 +- .../Mocks/TunnelAdapterDummy.swift | 2 +- .../PacketTunnelActorTests.swift | 340 ++++++++++++++ ios/PacketTunnelCoreTests/PingerTests.swift | 3 +- 22 files changed, 923 insertions(+), 237 deletions(-) delete mode 100644 ios/PacketTunnel/PacketTunnelProvider/State+Extensions.swift rename ios/PacketTunnelCore/Actor/{Actor+ConnectionMonitoring.swift => PacketTunnelActor+ConnectionMonitoring.swift} (100%) rename ios/PacketTunnelCore/Actor/{Actor+ErrorState.swift => PacketTunnelActor+ErrorState.swift} (100%) rename ios/PacketTunnelCore/Actor/{Actor+Extensions.swift => PacketTunnelActor+Extensions.swift} (100%) rename ios/PacketTunnelCore/Actor/{Actor+KeyPolicy.swift => PacketTunnelActor+KeyPolicy.swift} (97%) rename ios/PacketTunnelCore/Actor/{Actor+NetworkReachability.swift => PacketTunnelActor+NetworkReachability.swift} (100%) rename ios/PacketTunnelCore/Actor/{Actor+Public.swift => PacketTunnelActor+Public.swift} (97%) rename ios/PacketTunnelCore/Actor/{Actor+SleepCycle.swift => PacketTunnelActor+SleepCycle.swift} (100%) rename ios/PacketTunnelCore/Actor/{Actor.swift => PacketTunnelActor.swift} (99%) rename ios/{PacketTunnel/PacketTunnelProvider => PacketTunnelCore/IPC}/AppMessageHandler.swift (91%) delete mode 100644 ios/PacketTunnelCoreTests/ActorTests.swift create mode 100644 ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift create mode 100644 ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 7a8db6966edd..0f2bbcac6bd4 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -89,11 +89,11 @@ 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 */; }; + 5838321F2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838321E2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift */; }; + 583832212AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832202AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift */; }; + 583832232AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832222AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift */; }; + 583832252AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832242AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift */; }; + 583832272AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832262AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift */; }; 583832292AC3DF1300EA2071 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832282AC3DF1300EA2071 /* Command.swift */; }; 5838322B2AC3EF9600EA2071 /* CommandChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838322A2AC3EF9600EA2071 /* CommandChannel.swift */; }; 583D86482A2678DC0060D63B /* DeviceStateAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */; }; @@ -118,7 +118,6 @@ 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 */; }; @@ -146,7 +145,7 @@ 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 */; }; + 586C145A2AC4735F00245C01 /* PacketTunnelActor+Public.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */; }; 586E54FB27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */; }; 586E8DB82AAF4AC4007BF3DA /* Task+Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */; }; 5871167F2910035700D41AAC /* PreferencesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871167E2910035700D41AAC /* PreferencesInteractor.swift */; }; @@ -401,7 +400,6 @@ 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 */; }; @@ -416,11 +414,11 @@ 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 */; }; + 58FE25D42AA729B5003D1918 /* PacketTunnelActorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25D32AA729B5003D1918 /* PacketTunnelActorTests.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 */; }; + 58FE25DA2AA72A8F003D1918 /* PacketTunnelActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E9C3852A4EF1CB00CFDEAC /* PacketTunnelActor.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 */; }; @@ -430,7 +428,7 @@ 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 */; }; + 58FE25F42AA9D730003D1918 /* PacketTunnelActor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25F32AA9D730003D1918 /* PacketTunnelActor+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 */; }; @@ -489,6 +487,8 @@ 7ABE318D2A1CDD4500DF4963 /* UIFont+Weight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */; }; 7AE044BB2A935726003915D8 /* Routing.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A88DCD02A8FABBE00D2FF0E /* Routing.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7AE47E522A17972A000418DA /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE47E512A17972A000418DA /* AlertViewController.swift */; }; + 7AEF7F1A2AD00F52006FE45D /* AppMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */; }; + 7AEF7F1C2AD0150F006FE45D /* AppMessageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEF7F1B2AD0150F006FE45D /* AppMessageHandlerTests.swift */; }; 7AF6E5F02A95051E00F2679D /* RouterBlockDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */; }; 7AF6E5F12A95F4A500F2679D /* DurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FBFBF0291630700020E046 /* DurationTests.swift */; }; 7AF9BE992A4E0FE900DBFEDB /* MarkdownStylingOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE982A4E0FE900DBFEDB /* MarkdownStylingOptions.swift */; }; @@ -1131,11 +1131,11 @@ 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 = ""; }; + 5838321E2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+KeyPolicy.swift"; sourceTree = ""; }; + 583832202AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+NetworkReachability.swift"; sourceTree = ""; }; + 583832222AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+ErrorState.swift"; sourceTree = ""; }; + 583832242AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+ConnectionMonitoring.swift"; sourceTree = ""; }; + 583832262AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+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 = ""; }; @@ -1171,7 +1171,6 @@ 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 = ""; }; @@ -1198,7 +1197,7 @@ 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 = ""; }; + 586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+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 = ""; }; @@ -1370,7 +1369,7 @@ 58E7BA182A975DF70068EC3A /* RESTTransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportProvider.swift; sourceTree = ""; }; 58E973DD24850EB600096F90 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.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 = ""; }; + 58E9C3852A4EF1CB00CFDEAC /* PacketTunnelActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActor.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 = ""; }; @@ -1390,7 +1389,6 @@ 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 = ""; }; @@ -1402,12 +1400,12 @@ 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 = ""; }; + 58FE25D32AA729B5003D1918 /* PacketTunnelActorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorTests.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 = ""; }; + 58FE25F32AA9D730003D1918 /* PacketTunnelActor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+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 = ""; }; @@ -1458,6 +1456,8 @@ 7A9CCCB22A96302800DD6A34 /* TunnelCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelCoordinator.swift; sourceTree = ""; }; 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Weight.swift"; sourceTree = ""; }; 7AE47E512A17972A000418DA /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; + 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandler.swift; sourceTree = ""; }; + 7AEF7F1B2AD0150F006FE45D /* AppMessageHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandlerTests.swift; sourceTree = ""; }; 7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterBlockDelegate.swift; sourceTree = ""; }; 7AF9BE982A4E0FE900DBFEDB /* MarkdownStylingOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownStylingOptions.swift; sourceTree = ""; }; A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportStrategy.swift; sourceTree = ""; }; @@ -2202,26 +2202,26 @@ 5864AF802A9F52E3008BC928 /* Actor */ = { isa = PBXGroup; children = ( + 58BDEBA02A9CA14B00F578F2 /* AnyTask.swift */, + 58F3F3652AA086A400D3B0A4 /* AutoCancellingTask.swift */, 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 */, + 58E9C3852A4EF1CB00CFDEAC /* PacketTunnelActor.swift */, + 583832242AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift */, + 583832222AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift */, + 58FE25F32AA9D730003D1918 /* PacketTunnelActor+Extensions.swift */, + 5838321E2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift */, + 583832202AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift */, + 586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */, + 583832262AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift */, 58E7A0312AA0715100C57861 /* Protocols */, + 58ED3A132A7C199C0085CE65 /* StartOptions.swift */, + 5824030C2A811B0000163DE8 /* State.swift */, + 58342C032AAB61FB003BA12D /* State+Extensions.swift */, + 586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */, + 58DDA18E2ABC32380039C360 /* Timings.swift */, ); path = Actor; sourceTree = ""; @@ -2438,12 +2438,13 @@ 58C7A4432A863F490060C66F /* PacketTunnelCoreTests */ = { isa = PBXGroup; children = ( - 58FE25D32AA729B5003D1918 /* ActorTests.swift */, - 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */, - 58C7A46F2A8649ED0060C66F /* PingerTests.swift */, - 58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */, + 7AEF7F1B2AD0150F006FE45D /* AppMessageHandlerTests.swift */, 586C14572AC463BB00245C01 /* CommandChannelTests.swift */, 58EC067D2A8D2B0700BEB973 /* Mocks */, + 58FE25D32AA729B5003D1918 /* PacketTunnelActorTests.swift */, + 58C7A46F2A8649ED0060C66F /* PingerTests.swift */, + 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */, + 58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */, ); path = PacketTunnelCoreTests; sourceTree = ""; @@ -2451,6 +2452,7 @@ 58C9B8C52ABB23B400040B46 /* IPC */ = { isa = PBXGroup; children = ( + 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */, 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */, 5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */, 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */, @@ -2731,8 +2733,6 @@ isa = PBXGroup; children = ( 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */, - 58F775422AB9E3EF00425B47 /* AppMessageHandler.swift */, - 585B1FF12AB0BC69008AD470 /* State+Extensions.swift */, 580D6B912AB360BE00B2D6E0 /* DeviceCheck+BlockedStateReason.swift */, 5864AF7C2A9F4DC9008BC928 /* SettingsReader.swift */, 580D6B8D2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift */, @@ -3928,25 +3928,25 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 58FE25F42AA9D730003D1918 /* Actor+Extensions.swift in Sources */, + 58FE25F42AA9D730003D1918 /* PacketTunnelActor+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 */, + 5838321F2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift in Sources */, 58C7AF122ABD8480007EDD7A /* TunnelProviderReply.swift in Sources */, 58C7A4572A863FB90060C66F /* TunnelDeviceInfoProtocol.swift in Sources */, 58C7A4562A863FB90060C66F /* DefaultPathObserverProtocol.swift in Sources */, - 58FE25DA2AA72A8F003D1918 /* Actor.swift in Sources */, + 58FE25DA2AA72A8F003D1918 /* PacketTunnelActor.swift in Sources */, 58FE25E62AA738E8003D1918 /* TunnelAdapterProtocol.swift in Sources */, - 583832252AC318A100EA2071 /* Actor+ConnectionMonitoring.swift in Sources */, + 583832252AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift in Sources */, 58C7A4552A863FB90060C66F /* TunnelMonitor.swift in Sources */, 58C7AF182ABD84AB007EDD7A /* ProxyURLResponse.swift in Sources */, 58C7A4512A863FB50060C66F /* PingerProtocol.swift in Sources */, 583832292AC3DF1300EA2071 /* Command.swift in Sources */, - 583832232AC3181400EA2071 /* Actor+ErrorState.swift in Sources */, + 583832232AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift in Sources */, 58C7AF112ABD8480007EDD7A /* TunnelProviderMessage.swift in Sources */, 58C7AF162ABD84A8007EDD7A /* URLRequestProxy.swift in Sources */, 58FE25D72AA72A8F003D1918 /* State.swift in Sources */, @@ -3954,15 +3954,16 @@ 58C7A4592A863FB90060C66F /* WgStats.swift in Sources */, 7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */, 58FE25DB2AA72A8F003D1918 /* StartOptions.swift in Sources */, - 583832212AC3174700EA2071 /* Actor+NetworkReachability.swift in Sources */, + 583832212AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift in Sources */, 58FE25D82AA72A8F003D1918 /* ConfigurationBuilder.swift in Sources */, + 7AEF7F1A2AD00F52006FE45D /* AppMessageHandler.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 */, + 586C145A2AC4735F00245C01 /* PacketTunnelActor+Public.swift in Sources */, 58342C042AAB61FB003BA12D /* State+Extensions.swift in Sources */, - 583832272AC3193600EA2071 /* Actor+SleepCycle.swift in Sources */, + 583832272AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift in Sources */, 58FE25DC2AA72A8F003D1918 /* AnyTask.swift in Sources */, 58C7AF152ABD8480007EDD7A /* PacketTunnelRelay.swift in Sources */, 58FE25D92AA72A8F003D1918 /* AutoCancellingTask.swift in Sources */, @@ -3988,7 +3989,8 @@ 581F23AF2A8CF94D00788AB6 /* PingerMock.swift in Sources */, 58FE25F22AA77674003D1918 /* SettingsReaderStub.swift in Sources */, 58F7753D2AB8473200425B47 /* BlockedStateErrorMapperStub.swift in Sources */, - 58FE25D42AA729B5003D1918 /* ActorTests.swift in Sources */, + 58FE25D42AA729B5003D1918 /* PacketTunnelActorTests.swift in Sources */, + 7AEF7F1C2AD0150F006FE45D /* AppMessageHandlerTests.swift in Sources */, 58C7A4702A8649ED0060C66F /* PingerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4230,7 +4232,6 @@ 581DA2762A1E2FD10046ED47 /* WgKeyRotation.swift in Sources */, 580810E52A30E13A00B74552 /* DeviceStateAccessorProtocol.swift in Sources */, 580810E82A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */, - 585B1FF22AB0BC69008AD470 /* State+Extensions.swift in Sources */, 58C9B8CE2ABB252E00040B46 /* DeviceCheck.swift in Sources */, 58915D682A25FA080066445B /* DeviceCheckRemoteService.swift in Sources */, 58E9C3842A4EF15300CFDEAC /* WireGuardAdapter+Async.swift in Sources */, @@ -4250,7 +4251,6 @@ 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 */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift index a31c508288a6..bc6c713a9349 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift @@ -57,6 +57,10 @@ struct BlockedStateErrorMapper: BlockedStateErrorMapperProtocol { // packet tunnel provider. return .tunnelAdapter + case is PublicKeyError: + // Returned when there is an endpoint but its public key is invalid. + return .invalidPublicKey + default: // Everything else in case we introduce new errors and forget to handle them. return .unknown diff --git a/ios/PacketTunnel/PacketTunnelProvider/State+Extensions.swift b/ios/PacketTunnel/PacketTunnelProvider/State+Extensions.swift deleted file mode 100644 index 99862e152199..000000000000 --- a/ios/PacketTunnel/PacketTunnelProvider/State+Extensions.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// 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/PacketTunnelCore/Actor/ConfigurationBuilder.swift b/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift index c4a731aa7829..6ba03db89b3e 100644 --- a/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift +++ b/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift @@ -13,6 +13,15 @@ import struct WireGuardKitTypes.IPAddressRange import class WireGuardKitTypes.PrivateKey import class WireGuardKitTypes.PublicKey +/// Error returned when there is an endpoint but its public key is invalid. +public struct PublicKeyError: LocalizedError { + let endpoint: MullvadEndpoint + + public var errorDescription: String? { + "Public key is invalid, endpoint: \(endpoint)" + } +} + /// Struct building tunnel adapter configuration. struct ConfigurationBuilder { var privateKey: PrivateKey @@ -20,22 +29,28 @@ struct ConfigurationBuilder { var dns: SelectedDNSServers? var endpoint: MullvadEndpoint? - func makeConfiguration() -> TunnelAdapterConfiguration { + func makeConfiguration() throws -> TunnelAdapterConfiguration { return TunnelAdapterConfiguration( privateKey: privateKey, interfaceAddresses: interfaceAddresses, dns: dnsServers, - peer: peer + peer: try peer ) } private var peer: TunnelPeer? { - guard let endpoint else { return nil } + get throws { + guard let endpoint else { return nil } - return TunnelPeer( - endpoint: .ipv4(endpoint.ipv4Relay), - publicKey: PublicKey(rawValue: endpoint.publicKey)! - ) + guard let publicKey = PublicKey(rawValue: endpoint.publicKey) else { + throw PublicKeyError(endpoint: endpoint) + } + + return TunnelPeer( + endpoint: .ipv4(endpoint.ipv4Relay), + publicKey: publicKey + ) + } } private var dnsServers: [IPAddress] { diff --git a/ios/PacketTunnelCore/Actor/Actor+ConnectionMonitoring.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift similarity index 100% rename from ios/PacketTunnelCore/Actor/Actor+ConnectionMonitoring.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift diff --git a/ios/PacketTunnelCore/Actor/Actor+ErrorState.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift similarity index 100% rename from ios/PacketTunnelCore/Actor/Actor+ErrorState.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift diff --git a/ios/PacketTunnelCore/Actor/Actor+Extensions.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Extensions.swift similarity index 100% rename from ios/PacketTunnelCore/Actor/Actor+Extensions.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+Extensions.swift diff --git a/ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+KeyPolicy.swift similarity index 97% rename from ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+KeyPolicy.swift index 0cf8351523c2..3b708fb996d8 100644 --- a/ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+KeyPolicy.swift @@ -108,7 +108,7 @@ extension PacketTunnelActor { guard let self else { return } // Wait for key to propagate across relays. - try await Task.sleepUsingContinuousClock(for: timings.wgKeyPropagationDelay) +// try await Task.sleepUsingContinuousClock(for: timings.wgKeyPropagationDelay) // Enqueue task to change key policy. commandChannel.send(.switchKey) @@ -159,7 +159,7 @@ extension PacketTunnelActor { /** 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. + - Parameter keyPolicy: a reference to key policy held either in connection state or blocked state struct. - Returns: `true` when the policy was modified, otherwise `false`. */ private func setCurrentKeyPolicy(_ keyPolicy: inout KeyPolicy) -> Bool { diff --git a/ios/PacketTunnelCore/Actor/Actor+NetworkReachability.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift similarity index 100% rename from ios/PacketTunnelCore/Actor/Actor+NetworkReachability.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift diff --git a/ios/PacketTunnelCore/Actor/Actor+Public.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift similarity index 97% rename from ios/PacketTunnelCore/Actor/Actor+Public.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift index 7a50477c29d5..b54d397ca47d 100644 --- a/ios/PacketTunnelCore/Actor/Actor+Public.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift @@ -1,5 +1,5 @@ // -// Actor+Public.swift +// PacketTunnelActor+Public.swift // PacketTunnelCore // // Created by pronebird on 27/09/2023. diff --git a/ios/PacketTunnelCore/Actor/Actor+SleepCycle.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+SleepCycle.swift similarity index 100% rename from ios/PacketTunnelCore/Actor/Actor+SleepCycle.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+SleepCycle.swift diff --git a/ios/PacketTunnelCore/Actor/Actor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift similarity index 99% rename from ios/PacketTunnelCore/Actor/Actor.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index 18be2e2d409e..45f805eafd8b 100644 --- a/ios/PacketTunnelCore/Actor/Actor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -1,5 +1,5 @@ // -// Actor.swift +// PacketTunnelActor.swift // PacketTunnel // // Created by pronebird on 30/06/2023. diff --git a/ios/PacketTunnelCore/Actor/State+Extensions.swift b/ios/PacketTunnelCore/Actor/State+Extensions.swift index cfaed6cf0787..4f681ef52b62 100644 --- a/ios/PacketTunnelCore/Actor/State+Extensions.swift +++ b/ios/PacketTunnelCore/Actor/State+Extensions.swift @@ -7,6 +7,7 @@ // import Foundation +import MullvadTypes import class WireGuardKitTypes.PrivateKey extension State { @@ -36,6 +37,50 @@ 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 + } + + public 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 + } + } + // MARK: - Logging func logFormat() -> String { @@ -105,7 +150,7 @@ extension BlockedStateReason { return true case .noRelaysSatisfyingConstraints, .readSettings, .invalidAccount, .deviceRevoked, .tunnelAdapter, .unknown, - .deviceLoggedOut, .outdatedSchema: + .deviceLoggedOut, .outdatedSchema, .invalidPublicKey: return false } } diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift index 2c5cfd13b81f..4449372762d6 100644 --- a/ios/PacketTunnelCore/Actor/State.swift +++ b/ios/PacketTunnelCore/Actor/State.swift @@ -55,7 +55,11 @@ import class WireGuardKitTypes.PrivateKey `.connecting`, `.reconnecting`, `.error` can be interrupted if the tunnel is requested to stop, which should segue actor towards `.disconnected` state. */ -public enum State { +public enum State: Equatable { + public static func == (lhs: State, rhs: State) -> Bool { + lhs.name == rhs.name + } + /// Initial state at the time when actor is initialized but before the first connection attempt. case initial @@ -106,7 +110,7 @@ public struct ConnectionState { /// This is primarily used by packet tunnel for updating constraints in tunnel provider. public var relayConstraints: RelayConstraints - /// Last WG key read from setings. + /// Last WG key read from settings. /// Can be `nil` if moved to `keyPolicy`. public var currentKey: PrivateKey? @@ -188,6 +192,9 @@ public enum BlockedStateReason: String, Codable, Equatable { /// Tunnel adapter error. case tunnelAdapter + /// Invalid public key. + case invalidPublicKey + /// Unidentified reason. case unknown } diff --git a/ios/PacketTunnel/PacketTunnelProvider/AppMessageHandler.swift b/ios/PacketTunnelCore/IPC/AppMessageHandler.swift similarity index 91% rename from ios/PacketTunnel/PacketTunnelProvider/AppMessageHandler.swift rename to ios/PacketTunnelCore/IPC/AppMessageHandler.swift index 952fcb4bdeb7..2d57f3d34112 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/AppMessageHandler.swift +++ b/ios/PacketTunnelCore/IPC/AppMessageHandler.swift @@ -8,17 +8,16 @@ import Foundation import MullvadLogging -import PacketTunnelCore /** Actor handling packet tunnel IPC (app) messages and patching them through to the right facility. */ -struct AppMessageHandler { +public struct AppMessageHandler { private let logger = Logger(label: "AppMessageHandler") private let packetTunnelActor: PacketTunnelActor private let urlRequestProxy: URLRequestProxy - init(packetTunnelActor: PacketTunnelActor, urlRequestProxy: URLRequestProxy) { + public init(packetTunnelActor: PacketTunnelActor, urlRequestProxy: URLRequestProxy) { self.packetTunnelActor = packetTunnelActor self.urlRequestProxy = urlRequestProxy } @@ -34,7 +33,7 @@ struct AppMessageHandler { 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? { + public func handleAppMessage(_ messageData: Data) async -> Data? { guard let message = decodeMessage(messageData) else { return nil } logger.debug("Received app message: \(message)") @@ -51,7 +50,7 @@ struct AppMessageHandler { return await encodeReply(packetTunnelActor.state.packetTunnelStatus) case .privateKeyRotation: - packetTunnelActor.notifyKeyRotation(date: nil) + packetTunnelActor.notifyKeyRotation(date: Date()) return nil case let .reconnectTunnel(selectorResult): diff --git a/ios/PacketTunnelCoreTests/ActorTests.swift b/ios/PacketTunnelCoreTests/ActorTests.swift deleted file mode 100644 index 7b8cd6953875..000000000000 --- a/ios/PacketTunnelCoreTests/ActorTests.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// 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/AppMessageHandlerTests.swift b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift new file mode 100644 index 000000000000..5c81902d150a --- /dev/null +++ b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift @@ -0,0 +1,434 @@ +// +// AppMessageHandlerTests.swift +// PacketTunnelCoreTests +// +// Created by Jon Petersson on 2023-09-28. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Combine +@testable import MullvadREST +import MullvadTypes +import PacketTunnelCore +import RelaySelector +import WireGuardKitTypes +import XCTest + +final class AppMessageHandlerTests: XCTestCase { + private var stateSink: Combine.Cancellable? + + private lazy var urlRequestProxy: URLRequestProxy = { + let transportProvider = REST.AnyTransportProvider { + AnyTransport { Response(delay: 1, statusCode: 200, value: TimeResponse(dateTime: Date())) } + } + + return URLRequestProxy( + dispatchQueue: DispatchQueue(label: "AppMessageHandlerTests"), + transportProvider: transportProvider + ) + }() + + override func tearDown() async throws { + stateSink?.cancel() + } + + func testHandleAppMessageForTunnelStatus() async throws { + let actor = PacketTunnelActor.mock() + let appMessageHandler = createAppMessageHandler(actor: actor) + let reconnectingExpectation = expectation(description: "Expect reconnecting state") + + actor.start(options: StartOptions(launchSource: .app)) + actor.setErrorState(reason: .deviceRevoked) + + stateSink = await actor.$state + .receive(on: DispatchQueue.main) + .sink { newState in + switch newState { + case .error: + Task { + let reply = try? await appMessageHandler.handleAppMessage( + TunnelProviderMessage.getTunnelStatus.encode() + ) + let tunnelStatus = try? TunnelProviderReply(messageData: reply!) + + XCTAssertEqual(tunnelStatus?.value.blockedStateReason, .deviceRevoked) + reconnectingExpectation.fulfill() + } + default: + break + } + } + + await fulfillment(of: [reconnectingExpectation], timeout: 1) + } + + func testHandleAppMessageForURLRequest() async throws { + let actor = PacketTunnelActor.mock() + let appMessageHandler = createAppMessageHandler(actor: actor) + + actor.start(options: StartOptions(launchSource: .app)) + + let url = URL(string: "localhost")! + let urlRequest = ProxyURLRequest( + id: UUID(), + urlRequest: URLRequest(url: url) + )! + + let reply = try await appMessageHandler + .handleAppMessage(TunnelProviderMessage.sendURLRequest(urlRequest).encode()) + let urlResponse = try TunnelProviderReply(messageData: reply!) + + XCTAssertEqual(urlResponse.value.response?.url, url) + } + + func testHandleAppMessageForReconnectTunnel() async throws { + let actor = PacketTunnelActor.mock() + let appMessageHandler = createAppMessageHandler(actor: actor) + let reconnectingExpectation = expectation(description: "Expect reconnecting state") + + actor.start(options: StartOptions(launchSource: .app)) + + let relayConstraints = RelayConstraints(location: .only(.hostname("se", "sto", "se6-wireguard"))) + let selectorResult = try? RelaySelector.evaluate( + relays: sampleRelays, + constraints: relayConstraints, + numberOfFailedAttempts: 0 + ) + + _ = try? await appMessageHandler.handleAppMessage( + TunnelProviderMessage.reconnectTunnel(selectorResult).encode() + ) + + stateSink = await actor.$state + .receive(on: DispatchQueue.main) + .sink { newState in + switch newState { + case let .reconnecting(state): + XCTAssertEqual(state.selectedRelay.relay, selectorResult?.relay) + reconnectingExpectation.fulfill() + default: + break + } + } + + await fulfillment(of: [reconnectingExpectation], timeout: 1) + } + + func testHandleAppMessageForKeyRotation() async throws { + let actor = PacketTunnelActor.mock() + let appMessageHandler = createAppMessageHandler(actor: actor) + let connectedExpectation = expectation(description: "Expect connecting state") + + actor.start(options: StartOptions(launchSource: .app)) + + _ = try? await appMessageHandler.handleAppMessage( + TunnelProviderMessage.privateKeyRotation.encode() + ) + + stateSink = await actor.$state + .receive(on: DispatchQueue.main) + .sink { newState in + switch newState { + case let .connected(state): + XCTAssertNotNil(state.lastKeyRotation) + connectedExpectation.fulfill() + default: + break + } + } + + await fulfillment(of: [connectedExpectation], timeout: 3) + } +} + +extension AppMessageHandlerTests { + func createAppMessageHandler(actor: PacketTunnelActor) -> AppMessageHandler { + return AppMessageHandler( + packetTunnelActor: actor, + urlRequestProxy: urlRequestProxy + ) + } +} + +struct TimeResponse: Codable { + var dateTime: Date +} + +class AnyTransport: RESTTransport { + typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void + + private let handleRequest: () -> AnyResponse + + private let completionLock = NSLock() + private var completionHandlers: [UUID: CompletionHandler] = [:] + + init(block: @escaping () -> AnyResponse) { + handleRequest = block + } + + var name: String { + return "any-transport" + } + + func sendRequest( + _ request: URLRequest, + completion: @escaping (Data?, URLResponse?, Error?) -> Void + ) -> MullvadTypes.Cancellable { + let response = handleRequest() + let id = storeCompletion(completionHandler: completion) + + let dispatchWork = DispatchWorkItem { + let data = (try? response.encode()) ?? Data() + let httpResponse = HTTPURLResponse( + url: request.url!, + statusCode: response.statusCode, + httpVersion: "1.0", + headerFields: [:] + )! + self.sendCompletion(requestID: id, completion: .success((data, httpResponse))) + } + + DispatchQueue.global().asyncAfter(deadline: .now() + response.delay, execute: dispatchWork) + + return AnyCancellable { + dispatchWork.cancel() + + self.sendCompletion(requestID: id, completion: .failure(URLError(.cancelled))) + } + } + + private func storeCompletion(completionHandler: @escaping CompletionHandler) -> UUID { + return completionLock.withLock { + let id = UUID() + completionHandlers[id] = completionHandler + return id + } + } + + private func sendCompletion(requestID: UUID, completion: Result<(Data, URLResponse), Error>) { + let complationHandler = completionLock.withLock { + return completionHandlers.removeValue(forKey: requestID) + } + switch completion { + case let .success((data, response)): + complationHandler?(data, response, nil) + case let .failure(error): + complationHandler?(nil, nil, error) + } + } +} + +struct Response: AnyResponse { + var delay: TimeInterval + var statusCode: Int + var value: T + + func encode() throws -> Data { + return try REST.Coding.makeJSONEncoder().encode(value) + } +} + +protocol AnyResponse { + var delay: TimeInterval { get } + var statusCode: Int { get } + + func encode() throws -> Data +} + +private let portRanges: [[UInt16]] = [[4000, 4001], [5000, 5001]] + +private let sampleRelays = REST.ServerRelaysResponse( + locations: [ + "es-mad": REST.ServerLocation( + country: "Spain", + city: "Madrid", + latitude: 40.408566, + longitude: -3.69222 + ), + "se-got": REST.ServerLocation( + country: "Sweden", + city: "Gothenburg", + latitude: 57.70887, + longitude: 11.97456 + ), + "se-sto": REST.ServerLocation( + country: "Sweden", + city: "Stockholm", + latitude: 59.3289, + longitude: 18.0649 + ), + "ae-dxb": REST.ServerLocation( + country: "United Arab Emirates", + city: "Dubai", + latitude: 25.276987, + longitude: 55.296249 + ), + "jp-tyo": REST.ServerLocation( + country: "Japan", + city: "Tokyo", + latitude: 35.685, + longitude: 139.751389 + ), + "ca-tor": REST.ServerLocation( + country: "Canada", + city: "Toronto", + latitude: 43.666667, + longitude: -79.416667 + ), + "us-atl": REST.ServerLocation( + country: "USA", + city: "Atlanta, GA", + latitude: 40.73061, + longitude: -73.935242 + ), + "us-dal": REST.ServerLocation( + country: "USA", + city: "Dallas, TX", + latitude: 32.89748, + longitude: -97.040443 + ), + ], + wireguard: REST.ServerWireguardTunnels( + ipv4Gateway: .loopback, + ipv6Gateway: .loopback, + portRanges: portRanges, + relays: [ + REST.ServerRelay( + hostname: "es1-wireguard", + active: true, + owned: true, + location: "es-mad", + provider: "", + weight: 500, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: PrivateKey().publicKey.rawValue, + includeInCountry: true + ), + REST.ServerRelay( + hostname: "se10-wireguard", + active: true, + owned: true, + location: "se-got", + provider: "", + weight: 1000, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: PrivateKey().publicKey.rawValue, + includeInCountry: true + ), + REST.ServerRelay( + hostname: "se2-wireguard", + active: true, + owned: true, + location: "se-sto", + provider: "", + weight: 50, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: PrivateKey().publicKey.rawValue, + includeInCountry: true + ), + REST.ServerRelay( + hostname: "se6-wireguard", + active: true, + owned: true, + location: "se-sto", + provider: "", + weight: 100, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: PrivateKey().publicKey.rawValue, + includeInCountry: true + ), + REST.ServerRelay( + hostname: "us-dal-wg-001", + active: true, + owned: true, + location: "us-dal", + provider: "", + weight: 100, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: PrivateKey().publicKey.rawValue, + includeInCountry: true + ), + REST.ServerRelay( + hostname: "us-nyc-wg-301", + active: true, + owned: true, + location: "us-nyc", + provider: "", + weight: 100, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: PrivateKey().publicKey.rawValue, + includeInCountry: true + ), + ] + ), + bridge: REST.ServerBridges(shadowsocks: [ + REST.ServerShadowsocks(protocol: "tcp", port: 443, cipher: "aes-256-gcm", password: "mullvad"), + ], relays: [ + REST.BridgeRelay( + hostname: "se-sto-br-001", + active: true, + owned: true, + location: "se-sto", + provider: "31173", + ipv4AddrIn: .loopback, + weight: 100, + includeInCountry: true + ), + REST.BridgeRelay( + hostname: "jp-tyo-br-101", + active: true, + owned: true, + location: "jp-tyo", + provider: "M247", + ipv4AddrIn: .loopback, + weight: 1, + includeInCountry: true + ), + REST.BridgeRelay( + hostname: "ca-tor-ovpn-001", + active: false, + owned: false, + location: "ca-tor", + provider: "M247", + ipv4AddrIn: .loopback, + weight: 1, + includeInCountry: true + ), + REST.BridgeRelay( + hostname: "ae-dxb-ovpn-001", + active: true, + owned: false, + location: "ae-dxb", + provider: "M247", + ipv4AddrIn: .loopback, + weight: 100, + includeInCountry: true + ), + REST.BridgeRelay( + hostname: "us-atl-br-101", + active: true, + owned: false, + location: "us-atl", + provider: "100TB", + ipv4AddrIn: .loopback, + weight: 100, + includeInCountry: true + ), + REST.BridgeRelay( + hostname: "us-dal-br-101", + active: true, + owned: false, + location: "us-dal", + provider: "100TB", + ipv4AddrIn: .loopback, + weight: 100, + includeInCountry: true + ), + ]) +) diff --git a/ios/PacketTunnelCoreTests/Mocks/BlockedStateErrorMapperStub.swift b/ios/PacketTunnelCoreTests/Mocks/BlockedStateErrorMapperStub.swift index eecfcc692edc..936b864d4d2a 100644 --- a/ios/PacketTunnelCoreTests/Mocks/BlockedStateErrorMapperStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/BlockedStateErrorMapperStub.swift @@ -8,7 +8,7 @@ import Foundation import MullvadTypes -import PacketTunnelCore +@testable import PacketTunnelCore /// Blocked state error mapper stub that can be configured with a block to simulate a desired behavior. class BlockedStateErrorMapperStub: BlockedStateErrorMapperProtocol { diff --git a/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift b/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift index 8031ea888c18..cf6323cb4ba5 100644 --- a/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift +++ b/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift @@ -8,7 +8,7 @@ import Foundation import NetworkExtension -import PacketTunnelCore +@testable import PacketTunnelCore struct NetworkPathStub: NetworkPath { var status: NetworkExtension.NWPathStatus = .satisfied @@ -23,7 +23,7 @@ class DefaultPathObserverFake: DefaultPathObserverProtocol { private var innerPath: NetworkPath = NetworkPathStub() private var stateLock = NSLock() - private var defaultPathHandler: ((NetworkPath) -> Void)? + var defaultPathHandler: ((NetworkPath) -> Void)? func start(_ body: @escaping (NetworkPath) -> Void) { stateLock.withLock { diff --git a/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift b/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift index 4b7040f7566f..5b8557e48cd4 100644 --- a/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift +++ b/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift @@ -7,7 +7,7 @@ // import Foundation -import PacketTunnelCore +@testable import PacketTunnelCore /// Dummy tunnel adapter that does nothing and reports no errors. class TunnelAdapterDummy: TunnelAdapterProtocol { diff --git a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift new file mode 100644 index 000000000000..2b4861d494bc --- /dev/null +++ b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift @@ -0,0 +1,340 @@ +// +// PacketTunnelActorTests.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 PacketTunnelActorTests: XCTestCase { + private var stateSink: Combine.Cancellable? + + override func tearDown() async throws { + stateSink?.cancel() + } + + /** + Test a happy path start sequence. + + As actor should transition through the following states: .initial → .connecting → .connected + */ + func testStartGoesToConnectedInSequence() async throws { + let actor = PacketTunnelActor.mock() + + // As actor starts it should transition through the following states based on simulation: + // .initial → .connecting → .connected + 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 + } + } + + actor.start(options: StartOptions(launchSource: .app)) + + await fulfillment(of: allExpectations, timeout: 1, enforceOrder: true) + } + + func testStartIgnoresSubsequentStarts() async throws { + let actor = PacketTunnelActor.mock() + + // As actor starts it should transition through the following states based on simulation: + // .initial → .connecting → .connected + 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 + } + } + + actor.start(options: StartOptions(launchSource: .app)) + actor.start(options: StartOptions(launchSource: .app)) + + await fulfillment(of: allExpectations, timeout: 1, enforceOrder: true) + } + + /** + Test start sequence when reading settings yields an error indicating that device is locked. + This is common when network extenesion starts on boot with iOS. + + 1. The first attempt to read settings yields an error indicating that device is locked. + 2. An actor should set up a task to reconnect the tunnel periodically. + 3. The issue goes away on the second attempt to read settings. + 4. An actor should transition through `.connecting` towards`.connected` state. + */ + func testLockedDeviceErrorOnBoot() async throws { + let initialStateExpectation = expectation(description: "Expect initial state") + let errorStateExpectation = expectation(description: "Expect error state") + let connectingStateExpectation = expectation(description: "Expect connecting state") + let connectedStateExpectation = expectation(description: "Expect connected state") + let allExpectations = [ + initialStateExpectation, + errorStateExpectation, + connectingStateExpectation, + connectedStateExpectation, + ] + + let blockedStateMapper = BlockedStateErrorMapperStub { error in + if let error = error as? POSIXError, error.code == .EPERM { + return .deviceLocked + } else { + return .unknown + } + } + + var isFirstReadAttempt = true + let settingsReader = SettingsReaderStub { + if isFirstReadAttempt { + isFirstReadAttempt = false + throw POSIXError(.EPERM) + } else { + return Settings( + privateKey: PrivateKey(), + interfaceAddresses: [IPAddressRange(from: "127.0.0.1/32")!], + relayConstraints: RelayConstraints(), + dnsServers: .gateway + ) + } + } + + let actor = PacketTunnelActor.mock(blockedStateErrorMapper: blockedStateMapper, settingsReader: settingsReader) + + stateSink = await actor.$state + .receive(on: DispatchQueue.main) + .sink { newState in + switch newState { + case .initial: + initialStateExpectation.fulfill() + case .error: + errorStateExpectation.fulfill() + case .connecting: + connectingStateExpectation.fulfill() + case .connected: + connectedStateExpectation.fulfill() + default: + break + } + } + + actor.start(options: StartOptions(launchSource: .app)) + + await fulfillment(of: allExpectations, timeout: 1, enforceOrder: true) + } + + func testStopGoesToDisconnected() async throws { + let actor = PacketTunnelActor.mock() + let disconnectedStateExpectation = expectation(description: "Expect disconnected state") + let connectedStateExpectation = expectation(description: "Expect connected state") + + let expression: (State) -> Bool = { if case .connected = $0 { true } else { false } } + + await expect(expression, on: actor) { + connectedStateExpectation.fulfill() + } + + // Wait for the connected state to happen so it doesn't get coalesced immediately after the call to `actor.stop` + actor.start(options: StartOptions(launchSource: .app)) + await fulfillment(of: [connectedStateExpectation], timeout: 1) + + await expect(.disconnected, on: actor) { + disconnectedStateExpectation.fulfill() + } + actor.stop() + await fulfillment(of: [disconnectedStateExpectation], timeout: 1) + } + + func testStopIsNoopBeforeStart() async throws { + let actor = PacketTunnelActor.mock() + + actor.stop() + actor.stop() + actor.stop() + + switch await actor.state { + case .initial: break + default: XCTFail("Actor did not start, should be in .initial state") + } + } + + func testStopCancelsDefaultPathObserver() async throws { + let pathObserver = DefaultPathObserverFake() + let actor = PacketTunnelActor.mock(defaultPathObserver: pathObserver) + let connectedStateExpectation = expectation(description: "Connected state") + + let expression: (State) -> Bool = { if case .connected = $0 { true } else { false } } + + await expect(expression, on: actor) { + connectedStateExpectation.fulfill() + } + + actor.start(options: StartOptions(launchSource: .app)) + await fulfillment(of: [connectedStateExpectation], timeout: 1) + + let disconnectedStateExpectation = expectation(description: "Disconnected state") + + await expect(.disconnected, on: actor) { + disconnectedStateExpectation.fulfill() + } + actor.stop() + await fulfillment(of: [disconnectedStateExpectation], timeout: 1) + + XCTAssertNil(pathObserver.defaultPathHandler) + } + + func testSetErrorStateGetsCancelled() async throws { + let actor = PacketTunnelActor.mock() + let connectingStateExpectation = expectation(description: "Connecting state") + let disconnectedStateExpectation = expectation(description: "Disconnected state") + + stateSink = await actor.$state + .receive(on: DispatchQueue.main) + .sink { newState in + switch newState { + case .connecting: + // Guarantee that the task doesn't set the actor to error state before it's cancelled + let task = Task.detached { + try await Task.sleep(duration: .seconds(1)) + actor.setErrorState(reason: .readSettings) + } + task.cancel() + connectingStateExpectation.fulfill() + case .error: + XCTFail("Should not go to error state") + case .disconnected: + disconnectedStateExpectation.fulfill() + default: + break + } + } + + actor.start(options: StartOptions(launchSource: .app)) + await fulfillment(of: [connectingStateExpectation], timeout: 1) + actor.stop() + await fulfillment(of: [disconnectedStateExpectation], timeout: 1) + } + + func testReconnectIsNoopBeforeConnecting() async throws { + let actor = PacketTunnelActor.mock() + let initialStateExpectation = expectation(description: "Expect initial state") + + stateSink = await actor.$state.receive(on: DispatchQueue.main).sink { newState in + if case .initial = newState { + initialStateExpectation.fulfill() + return + } + XCTFail("Should not change states before starting the actor") + } + + actor.reconnect(to: .random) + + await fulfillment(of: [initialStateExpectation], timeout: 1) + } + + func testCannotReconnectAfterStopping() async throws { + let actor = PacketTunnelActor.mock() + + let disconnectedStateExpectation = expectation(description: "Expect disconnected state") + + await expect(.disconnected, on: actor) { + disconnectedStateExpectation.fulfill() + } + + actor.start(options: StartOptions(launchSource: .app)) + actor.stop() + + await fulfillment(of: [disconnectedStateExpectation], timeout: 1) + + await expect(.initial, on: actor) { + XCTFail("Should not be trying to reconnect after stopping") + } + actor.reconnect(to: .random) + } + + func testReconnectionStopsTunnelMonitor() async throws { + let stopMonitorExpectation = expectation(description: "Tunnel monitor stop") + + let tunnelMonitor = TunnelMonitorStub { command, dispatcher in + switch command { + case .start: + dispatcher.send(.connectionEstablished, after: .milliseconds(10)) + case .stop: + stopMonitorExpectation.fulfill() + } + } + let actor = PacketTunnelActor.mock(tunnelMonitor: tunnelMonitor) + let connectedExpectation = expectation(description: "Expect connected state") + + let expression: (State) -> Bool = { if case .connected = $0 { return true } else { return false } } + await expect(expression, on: actor) { + connectedExpectation.fulfill() + } + actor.start(options: StartOptions(launchSource: .app)) + await fulfillment(of: [connectedExpectation], timeout: 1) + + // Cancel the state sink to avoid overfulfilling the connected expectation + stateSink?.cancel() + + actor.reconnect(to: .random) + await fulfillment(of: [stopMonitorExpectation], timeout: 1) + } +} + +extension PacketTunnelActorTests { + func expect(_ state: State, on actor: PacketTunnelActor, _ action: @escaping () -> Void) async { + stateSink = await actor.$state.receive(on: DispatchQueue.main).sink { newState in + if state == newState { + action() + } + } + } + + func expect( + _ expression: @escaping (State) -> Bool, + on actor: PacketTunnelActor, + _ action: @escaping () -> Void + ) async { + stateSink = await actor.$state.receive(on: DispatchQueue.main).sink { newState in + if expression(newState) { + action() + } + } + } +} diff --git a/ios/PacketTunnelCoreTests/PingerTests.swift b/ios/PacketTunnelCoreTests/PingerTests.swift index d77e9c4e5df8..115232b05895 100644 --- a/ios/PacketTunnelCoreTests/PingerTests.swift +++ b/ios/PacketTunnelCoreTests/PingerTests.swift @@ -7,7 +7,8 @@ // import Network -import PacketTunnelCore +@testable import PacketTunnelCore +import WireGuardKitTypes import XCTest final class PingerTests: XCTestCase {