From c5acfb9c1bc788d98b2169d717d04e3019284451 Mon Sep 17 00:00:00 2001 From: mojganii Date: Thu, 25 Jul 2024 10:45:45 +0200 Subject: [PATCH] Apply multihop for normal connection --- .../MullvadREST/RelaySelectorStub.swift | 5 +- .../Relay/MultihopDecisionFlow.swift | 8 +- ios/MullvadREST/Relay/RelayPicking.swift | 5 +- .../Relay/RelaySelectorProtocol.swift | 10 +- ios/MullvadVPN.xcodeproj/project.pbxproj | 34 +++--- ios/MullvadVPN/AppDelegate.swift | 9 +- .../SimulatorTunnelProviderHost.swift | 3 +- .../TunnelManager/Tunnel+Settings.swift | 26 +++++ .../TunnelManager/TunnelManager.swift | 40 +++---- .../TunnelManager/TunnelState+UI.swift | 12 +-- .../TunnelManager/TunnelState.swift | 23 +++- .../Tunnel/TunnelControlView.swift | 9 +- .../Tunnel/TunnelControlViewModel.swift | 10 +- .../Tunnel/TunnelViewController.swift | 6 +- .../PacketTunnelActorReducerTests.swift | 2 +- .../TunnelSettingsStrategyTests.swift | 101 ++++++++++++++++++ .../WireGuardAdapter/WgAdapter.swift | 36 +++++++ .../WireGuardAdapter+Async.swift | 12 +++ .../Actor/PacketTunnelActor+PostQuantum.swift | 4 +- .../Actor/PacketTunnelActor.swift | 101 +++++++++++------- .../Actor/PacketTunnelActorCommand.swift | 2 +- .../Protocols/TunnelAdapterProtocol.swift | 6 ++ ios/PacketTunnelCore/Actor/StartOptions.swift | 3 +- .../Actor/State+Extensions.swift | 11 +- ios/PacketTunnelCore/Actor/State.swift | 4 +- .../AppMessageHandlerTests.swift | 6 +- .../Mocks/TunnelAdapterDummy.swift | 5 + 27 files changed, 366 insertions(+), 127 deletions(-) create mode 100644 ios/MullvadVPN/TunnelManager/Tunnel+Settings.swift create mode 100644 ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelSettingsStrategyTests.swift diff --git a/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift index f2f8952df4f0..f404aff1e275 100644 --- a/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift +++ b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift @@ -43,12 +43,13 @@ extension RelaySelectorStub { cityCode: "got", latitude: 0, longitude: 0 - ), retryAttempts: 0 + ) ) return SelectedRelays( entry: cityRelay, - exit: cityRelay + exit: cityRelay, + retryAttempt: 0 ) } } diff --git a/ios/MullvadREST/Relay/MultihopDecisionFlow.swift b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift index fa8431ed9901..53d8d1a8bc9d 100644 --- a/ios/MullvadREST/Relay/MultihopDecisionFlow.swift +++ b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift @@ -37,7 +37,7 @@ struct OneToOne: MultihopDecisionFlow { let entryMatch = try relayPicker.findBestMatch(from: entryCandidates) let exitMatch = try relayPicker.findBestMatch(from: exitCandidates) - return SelectedRelays(entry: entryMatch, exit: exitMatch) + return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount) } func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { @@ -70,11 +70,11 @@ struct OneToMany: MultihopDecisionFlow { case let (1, count) where count > 1: let entryMatch = try multihopPicker.findBestMatch(from: entryCandidates) let exitMatch = try multihopPicker.exclude(relay: entryMatch, from: exitCandidates) - return SelectedRelays(entry: entryMatch, exit: exitMatch) + return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount) default: let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates) let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates) - return SelectedRelays(entry: entryMatch, exit: exitMatch) + return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount) } } @@ -107,7 +107,7 @@ struct ManyToMany: MultihopDecisionFlow { let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates) let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates) - return SelectedRelays(entry: entryMatch, exit: exitMatch) + return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount) } func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { diff --git a/ios/MullvadREST/Relay/RelayPicking.swift b/ios/MullvadREST/Relay/RelayPicking.swift index eec1003a1ca5..0877d8908e3e 100644 --- a/ios/MullvadREST/Relay/RelayPicking.swift +++ b/ios/MullvadREST/Relay/RelayPicking.swift @@ -30,8 +30,7 @@ extension RelayPicking { return SelectedRelay( endpoint: match.endpoint, hostname: match.relay.hostname, - location: match.location, - retryAttempts: connectionAttemptCount + location: match.location ) } } @@ -50,7 +49,7 @@ struct SinglehopPicker: RelayPicking { let match = try findBestMatch(from: candidates) - return SelectedRelays(entry: nil, exit: match) + return SelectedRelays(entry: nil, exit: match, retryAttempt: connectionAttemptCount) } } diff --git a/ios/MullvadREST/Relay/RelaySelectorProtocol.swift b/ios/MullvadREST/Relay/RelaySelectorProtocol.swift index 390757c3ddf0..c1de0951aa56 100644 --- a/ios/MullvadREST/Relay/RelaySelectorProtocol.swift +++ b/ios/MullvadREST/Relay/RelaySelectorProtocol.swift @@ -25,15 +25,11 @@ public struct SelectedRelay: Equatable, Codable { /// Relay geo location. public let location: Location - /// Number of retried attempts to connect to a relay. - public let retryAttempts: UInt - /// Designated initializer. - public init(endpoint: MullvadEndpoint, hostname: String, location: Location, retryAttempts: UInt) { + public init(endpoint: MullvadEndpoint, hostname: String, location: Location) { self.endpoint = endpoint self.hostname = hostname self.location = location - self.retryAttempts = retryAttempts } } @@ -46,10 +42,12 @@ extension SelectedRelay: CustomDebugStringConvertible { public struct SelectedRelays: Equatable, Codable { public let entry: SelectedRelay? public let exit: SelectedRelay + public let retryAttempt: UInt - public init(entry: SelectedRelay?, exit: SelectedRelay) { + public init(entry: SelectedRelay?, exit: SelectedRelay, retryAttempt: UInt) { self.entry = entry self.exit = exit + self.retryAttempt = retryAttempt } } diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index e4b9a2a532f4..fcea4faa220e 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -477,7 +477,6 @@ 7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */; }; 7A3353972AAA0F8600F0A71C /* OperationBlockObserverSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */; }; 7A3AD5012C1068A800E9AD90 /* RelayPicking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */; }; - 7A3EFAAB2BDFDAE800318736 /* RelaySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */; }; 7A3FD1B52AD4465A0042BEA6 /* AppMessageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */; }; 7A3FD1B72AD54ABD0042BEA6 /* AnyTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */; }; 7A3FD1B82AD54AE60042BEA6 /* TimeServerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */; }; @@ -598,10 +597,8 @@ 7ADCB2D82B6A6EB300C88F89 /* AnyRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADCB2D72B6A6EB300C88F89 /* AnyRelay.swift */; }; 7ADCB2DA2B6A730400C88F89 /* IPOverrideRepositoryStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADCB2D92B6A730400C88F89 /* IPOverrideRepositoryStub.swift */; }; 7AE044BB2A935726003915D8 /* Routing.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A88DCD02A8FABBE00D2FF0E /* Routing.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 7AE2414A2C20682B0076CE33 /* FormsheetPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE241482C20682B0076CE33 /* FormsheetPresentationController.swift */; }; 7AE90B682C2D726000375A60 /* NSParagraphStyle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE90B672C2D726000375A60 /* NSParagraphStyle+Extensions.swift */; }; 7AEBA52A2C2179F20018BEC5 /* RelaySelectorWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEBA5292C2179F20018BEC5 /* RelaySelectorWrapper.swift */; }; - 7AEBA52C2C22C65B0018BEC5 /* TimeInterval+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEBA52B2C22C65B0018BEC5 /* TimeInterval+Timeout.swift */; }; 7AED35CC2BD13F60002A67D1 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; 7AED35CD2BD13FC4002A67D1 /* ApplicationTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A072A33850E00100D75 /* ApplicationTarget.swift */; }; 7AEF7F1A2AD00F52006FE45D /* AppMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */; }; @@ -855,12 +852,15 @@ F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */; }; F0164EC32B4C49D30020268D /* ShadowsocksLoaderStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */; }; F0164ED12B4F2DCB0020268D /* AccessMethodIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */; }; + F01DAE332C2B032A00521E46 /* RelaySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01DAE322C2B032A00521E46 /* RelaySelection.swift */; }; F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */; }; F028A56C2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */; }; F02F41A02B9723AF00625A4F /* AddLocationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419A2B9723AE00625A4F /* AddLocationsViewController.swift */; }; F02F41A12B9723AF00625A4F /* AddLocationsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419B2B9723AE00625A4F /* AddLocationsDataSource.swift */; }; F02F41A22B9723AF00625A4F /* AddLocationsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419C2B9723AF00625A4F /* AddLocationsCoordinator.swift */; }; F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */; }; + F03A69F72C2AD2D6000E2E7E /* TimeInterval+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03A69F62C2AD2D5000E2E7E /* TimeInterval+Timeout.swift */; }; + F03A69F92C2AD414000E2E7E /* FormsheetPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03A69F82C2AD413000E2E7E /* FormsheetPresentationController.swift */; }; F04413612BA45CD70018A6EE /* CustomListLocationNodeBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04413602BA45CD70018A6EE /* CustomListLocationNodeBuilder.swift */; }; F04413622BA45CE30018A6EE /* CustomListLocationNodeBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04413602BA45CD70018A6EE /* CustomListLocationNodeBuilder.swift */; }; F04FBE612A8379EE009278D7 /* AppPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04FBE602A8379EE009278D7 /* AppPreferences.swift */; }; @@ -900,6 +900,9 @@ F09D04BD2AEBB7C5003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; }; F09D04C02AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */; }; F09D04C12AF39EA2003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; }; + F0A0868E2C22D60100BF83E7 /* Tunnel+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A0868D2C22D60100BF83E7 /* Tunnel+Settings.swift */; }; + F0A086902C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */; }; + F0A086922C22E0F100BF83E7 /* Tunnel+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A0868D2C22D60100BF83E7 /* Tunnel+Settings.swift */; }; F0ACE30D2BE4E478006D5333 /* MullvadMockData.h in Headers */ = {isa = PBXBuildFile; fileRef = F0ACE30A2BE4E478006D5333 /* MullvadMockData.h */; settings = {ATTRIBUTES = (Public, ); }; }; F0ACE3102BE4E478006D5333 /* MullvadMockData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */; }; F0ACE3112BE4E478006D5333 /* MullvadMockData.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1841,7 +1844,6 @@ 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelInfo.swift; sourceTree = ""; }; 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserverSupport.swift; sourceTree = ""; }; 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicking.swift; sourceTree = ""; }; - 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelection.swift; sourceTree = ""; }; 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandlerTests.swift; sourceTree = ""; }; 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = ""; }; 7A45CFC22C05FF2F00D80B21 /* ScreenshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotTests.swift; sourceTree = ""; }; @@ -1944,10 +1946,8 @@ 7AD0AA202AD6CB0000119E10 /* URLRequestProxyStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyStub.swift; sourceTree = ""; }; 7ADCB2D72B6A6EB300C88F89 /* AnyRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyRelay.swift; sourceTree = ""; }; 7ADCB2D92B6A730400C88F89 /* IPOverrideRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideRepositoryStub.swift; sourceTree = ""; }; - 7AE241482C20682B0076CE33 /* FormsheetPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormsheetPresentationController.swift; sourceTree = ""; }; 7AE90B672C2D726000375A60 /* NSParagraphStyle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSParagraphStyle+Extensions.swift"; sourceTree = ""; }; 7AEBA5292C2179F20018BEC5 /* RelaySelectorWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapper.swift; sourceTree = ""; }; - 7AEBA52B2C22C65B0018BEC5 /* TimeInterval+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Timeout.swift"; sourceTree = ""; }; 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandler.swift; sourceTree = ""; }; 7AF10EB12ADE859200C090B9 /* AlertViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; 7AF10EB32ADE85BC00C090B9 /* RelayFilterCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayFilterCoordinator.swift; sourceTree = ""; }; @@ -2106,12 +2106,15 @@ F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoader.swift; sourceTree = ""; }; F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoaderStub.swift; sourceTree = ""; }; F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodIterator.swift; sourceTree = ""; }; + F01DAE322C2B032A00521E46 /* RelaySelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelaySelection.swift; sourceTree = ""; }; F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewController.swift; sourceTree = ""; }; F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddCreditSucceededViewController.swift; sourceTree = ""; }; F02F419A2B9723AE00625A4F /* AddLocationsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLocationsViewController.swift; sourceTree = ""; }; F02F419B2B9723AE00625A4F /* AddLocationsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLocationsDataSource.swift; sourceTree = ""; }; F02F419C2B9723AF00625A4F /* AddLocationsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLocationsCoordinator.swift; sourceTree = ""; }; F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = ""; }; + F03A69F62C2AD2D5000E2E7E /* TimeInterval+Timeout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Timeout.swift"; sourceTree = ""; }; + F03A69F82C2AD413000E2E7E /* FormsheetPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormsheetPresentationController.swift; sourceTree = ""; }; F04413602BA45CD70018A6EE /* CustomListLocationNodeBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListLocationNodeBuilder.swift; sourceTree = ""; }; F04DD3D72C130DF600E03E28 /* TunnelSettingsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManager.swift; sourceTree = ""; }; F04FBE602A8379EE009278D7 /* AppPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreferences.swift; sourceTree = ""; }; @@ -2143,6 +2146,8 @@ F09D04BA2AE95396003D4F89 /* URLSessionStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionStub.swift; sourceTree = ""; }; F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionService.swift; sourceTree = ""; }; F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionServiceTests.swift; sourceTree = ""; }; + F0A0868D2C22D60100BF83E7 /* Tunnel+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tunnel+Settings.swift"; sourceTree = ""; }; + F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsStrategyTests.swift; sourceTree = ""; }; F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadMockData.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F0ACE30A2BE4E478006D5333 /* MullvadMockData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadMockData.h; sourceTree = ""; }; F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProxyFactory.swift; sourceTree = ""; }; @@ -2542,6 +2547,7 @@ 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */, 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */, A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */, + F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */, 44BB5F992BE529FE002520EB /* TunnelStateTests.swift */, A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */, A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */, @@ -2662,6 +2668,7 @@ 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */, 58E0A98727C8F46300FE6BDD /* Tunnel.swift */, 5875960926F371FC00BF6711 /* Tunnel+Messaging.swift */, + F0A0868D2C22D60100BF83E7 /* Tunnel+Settings.swift */, 5878A27229091D6D0096FC88 /* TunnelBlockObserver.swift */, 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */, 58968FAD28743E2000B799DC /* TunnelInteractor.swift */, @@ -2781,7 +2788,7 @@ F0BE65362B9F136A005CC385 /* LocationSectionHeaderView.swift */, 5888AD86227B17950051EB06 /* LocationViewController.swift */, 7AB3BEB42BD7A6CB00E34384 /* LocationViewControllerWrapper.swift */, - 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */, + F01DAE322C2B032A00521E46 /* RelaySelection.swift */, ); path = SelectLocation; sourceTree = ""; @@ -3865,7 +3872,7 @@ 7AE241492C20682B0076CE33 /* Presentation controllers */ = { isa = PBXGroup; children = ( - 7AE241482C20682B0076CE33 /* FormsheetPresentationController.swift */, + F03A69F82C2AD413000E2E7E /* FormsheetPresentationController.swift */, ); path = "Presentation controllers"; sourceTree = ""; @@ -3873,7 +3880,7 @@ 7AEBA52D2C2310D20018BEC5 /* Extensions */ = { isa = PBXGroup; children = ( - 7AEBA52B2C22C65B0018BEC5 /* TimeInterval+Timeout.swift */, + F03A69F62C2AD2D5000E2E7E /* TimeInterval+Timeout.swift */, ); path = Extensions; sourceTree = ""; @@ -5242,6 +5249,7 @@ A9A5F9EB2ACB05160083449F /* CharacterSet+IPAddress.swift in Sources */, F0D8825C2B04F70E00D3EF9A /* OutgoingConnectionData.swift in Sources */, 44B3C43D2C00CBBD0079782C /* PacketTunnelActorReducerTests.swift in Sources */, + F0A086902C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift in Sources */, A9A5F9EC2ACB05160083449F /* CodingErrors+CustomErrorDescription.swift in Sources */, A9A5F9ED2ACB05160083449F /* NSRegularExpression+IPAddress.swift in Sources */, A9A5F9EE2ACB05160083449F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */, @@ -5263,6 +5271,7 @@ A9A5F9F62ACB05160083449F /* TunnelStatusNotificationProvider.swift in Sources */, A9A5F9F72ACB05160083449F /* NotificationProviderProtocol.swift in Sources */, A9A5F9F82ACB05160083449F /* NotificationProviderIdentifier.swift in Sources */, + F0A086922C22E0F100BF83E7 /* Tunnel+Settings.swift in Sources */, A9A5F9F92ACB05160083449F /* NotificationProvider.swift in Sources */, A9A5F9FA2ACB05160083449F /* InAppNotificationDescriptor.swift in Sources */, A9A5F9FB2ACB05160083449F /* InAppNotificationProvider.swift in Sources */, @@ -5502,6 +5511,7 @@ 5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */, 58E511E628DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */, 58C76A0B2A338E4300100D75 /* BackgroundTask.swift in Sources */, + F0A0868E2C22D60100BF83E7 /* Tunnel+Settings.swift in Sources */, 7A9CCCC32A96302800DD6A34 /* ApplicationCoordinator.swift in Sources */, 5864AF0729C78843005B0CD9 /* SettingsCellFactory.swift in Sources */, 587B75412668FD7800DEF7E9 /* AccountExpirySystemNotificationProvider.swift in Sources */, @@ -5541,6 +5551,7 @@ 7A9CCCB32A96302800DD6A34 /* WelcomeCoordinator.swift in Sources */, 587B753B2666467500DEF7E9 /* NotificationBannerView.swift in Sources */, 5827B0922B0CAB2800CCBBA1 /* MethodSettingsViewController.swift in Sources */, + F01DAE332C2B032A00521E46 /* RelaySelection.swift in Sources */, 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */, 7A516C2E2B6D357500BBD33D /* URL+Scoping.swift in Sources */, 5878A27529093A310096FC88 /* StorePaymentEvent.swift in Sources */, @@ -5589,6 +5600,7 @@ 58CEB30C2AFD586600E6E088 /* DynamicBackgroundConfiguration.swift in Sources */, 587B7536266528A200DEF7E9 /* NotificationManager.swift in Sources */, 5820EDA9288FE064006BF4E4 /* DeviceManagementInteractor.swift in Sources */, + F03A69F92C2AD414000E2E7E /* FormsheetPresentationController.swift in Sources */, 7A9CCCB92A96302800DD6A34 /* LocationCoordinator.swift in Sources */, 58FB865A26EA214400F188BC /* RelayCacheTrackerObserver.swift in Sources */, 7AE90B682C2D726000375A60 /* NSParagraphStyle+Extensions.swift in Sources */, @@ -5710,7 +5722,6 @@ 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */, 58B26E242943520C00D5980C /* NotificationProviderProtocol.swift in Sources */, 5877F94E2A0A59AA0052D9E9 /* NotificationResponse.swift in Sources */, - 7AE2414A2C20682B0076CE33 /* FormsheetPresentationController.swift in Sources */, 7A6389E52B7E4247008E77E1 /* EditCustomListCoordinator.swift in Sources */, 58677712290976FB006F721F /* SettingsInteractor.swift in Sources */, 58EF875D2B1638BF00C098B2 /* ProxyConfigurationTesterProtocol.swift in Sources */, @@ -5720,7 +5731,6 @@ 5878F50029CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift in Sources */, 7A28826D2BAAC9DE00FD9F20 /* IPOverrideHeaderView.swift in Sources */, A98502032B627B120061901E /* LocalNetworkProbe.swift in Sources */, - 7A3EFAAB2BDFDAE800318736 /* RelaySelection.swift in Sources */, 7A6F2FA92AFD0842006D0856 /* CustomDNSDataSource.swift in Sources */, 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */, 5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */, @@ -6093,11 +6103,11 @@ F0ACE31E2BE4E4F2006D5333 /* AccountsProxy+Stubs.swift in Sources */, F0ACE3202BE4E4F2006D5333 /* AccessTokenManager+Stubs.swift in Sources */, F0ACE32C2BE4E77E006D5333 /* DeviceMock.swift in Sources */, - 7AEBA52C2C22C65B0018BEC5 /* TimeInterval+Timeout.swift in Sources */, F0ACE3222BE4E4F2006D5333 /* APIProxy+Stubs.swift in Sources */, F0ACE3332BE516F1006D5333 /* RESTRequestExecutor+Stubs.swift in Sources */, F0ACE32D2BE4E784006D5333 /* AccountMock.swift in Sources */, 7A52F96A2C1735AE00B133B9 /* RelaySelectorStub.swift in Sources */, + F03A69F72C2AD2D6000E2E7E /* TimeInterval+Timeout.swift in Sources */, F0ACE32F2BE4EA8B006D5333 /* MockProxyFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 769954e553e2..124ec5751739 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -79,6 +79,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD ipOverrideRepository: ipOverrideRepository ) + let constraintsUpdater = RelayConstraintsUpdater() + let multihopListener = MultihopStateListener() + let multihopUpdater = MultihopUpdater(listener: multihopListener) + relayCacheTracker = RelayCacheTracker( relayCache: ipOverrideWrapper, application: application, @@ -86,11 +90,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD ) addressCacheTracker = AddressCacheTracker(application: application, apiProxy: apiProxy, store: addressCache) - tunnelStore = TunnelStore(application: application) - let constraintsUpdater = RelayConstraintsUpdater() - let multihopListener = MultihopStateListener() - let multihopUpdater = MultihopUpdater(listener: multihopListener) + tunnelStore = TunnelStore(application: application) let relaySelector = RelaySelectorWrapper( relayCache: ipOverrideWrapper, diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift index 08b49d4b0585..193f79b9878b 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift @@ -177,7 +177,8 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { networkReachability: .reachable, connectionAttemptCount: 0, transportLayer: .udp, - remotePort: selectedRelays.exit.endpoint.ipv4Relay.port, // TODO: Multihop + remotePort: selectedRelays.entry?.endpoint.ipv4Relay.port ?? selectedRelays.exit.endpoint.ipv4Relay + .port, isPostQuantum: settings.tunnelQuantumResistance.isEnabled ) ) diff --git a/ios/MullvadVPN/TunnelManager/Tunnel+Settings.swift b/ios/MullvadVPN/TunnelManager/Tunnel+Settings.swift new file mode 100644 index 000000000000..80c91b761892 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/Tunnel+Settings.swift @@ -0,0 +1,26 @@ +// +// Tunnel+Settings.swift +// MullvadVPN +// +// Created by Mojgan on 2024-06-19. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadLogging +import MullvadSettings + +protocol TunnelSettingsStrategyProtocol { + func shouldReconnectToNewRelay(oldSettings: LatestTunnelSettings, newSettings: LatestTunnelSettings) -> Bool +} + +struct TunnelSettingsStrategy: TunnelSettingsStrategyProtocol { + func shouldReconnectToNewRelay(oldSettings: LatestTunnelSettings, newSettings: LatestTunnelSettings) -> Bool { + switch (oldSettings, newSettings) { + case let (old, new) where old.relayConstraints != new.relayConstraints, + let (old, new) where old.tunnelMultihopState != new.tunnelMultihopState: + true + default: + false + } + } +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index d6d4be4d0616..bc3dff4fc7bb 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -939,16 +939,16 @@ final class TunnelManager: StorePaymentObserver { let operation = AsyncBlockOperation(dispatchQueue: internalQueue) { let currentSettings = self._tunnelSettings var updatedSettings = self._tunnelSettings + let settingsStrategy = TunnelSettingsStrategy() modificationBlock(&updatedSettings) - // Select new relay only when relay constraints change. - let currentConstraints = currentSettings.relayConstraints - let updatedConstraints = updatedSettings.relayConstraints - let selectNewRelay = currentConstraints != updatedConstraints - self.setSettings(updatedSettings, persist: true) - self.reconnectTunnel(selectNewRelay: selectNewRelay, completionHandler: nil) + self.reconnectTunnel( + selectNewRelay: settingsStrategy + .shouldReconnectToNewRelay(oldSettings: currentSettings, newSettings: updatedSettings), + completionHandler: nil + ) } operation.completionBlock = { @@ -1146,27 +1146,27 @@ extension TunnelManager { ``` func delay(seconds: UInt) async throws { - try await Task.sleep(nanoseconds: UInt64(seconds) * 1_000_000_000) + try await Task.sleep(nanoseconds: UInt64(seconds) * 1_000_000_000) } Task { - print("Wait 5 seconds") - try await delay(seconds: 5) + print("Wait 5 seconds") + try await delay(seconds: 5) - print("Simulate active account") - self.tunnelManager.simulateAccountExpiration(option: .active) - try await delay(seconds: 5) + print("Simulate active account") + self.tunnelManager.simulateAccountExpiration(option: .active) + try await delay(seconds: 5) - print("Simulate close to expiry") - self.tunnelManager.simulateAccountExpiration(option: .closeToExpiry) - try await delay(seconds: 10) + print("Simulate close to expiry") + self.tunnelManager.simulateAccountExpiration(option: .closeToExpiry) + try await delay(seconds: 10) - print("Simulate expired account") - self.tunnelManager.simulateAccountExpiration(option: .expired) - try await delay(seconds: 5) + print("Simulate expired account") + self.tunnelManager.simulateAccountExpiration(option: .expired) + try await delay(seconds: 5) - print("Simulate active account") - self.tunnelManager.simulateAccountExpiration(option: .active) + print("Simulate active account") + self.tunnelManager.simulateAccountExpiration(option: .active) } ``` diff --git a/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift b/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift index 3422c8602d88..1f524f56e8ad 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift @@ -187,8 +187,8 @@ extension TunnelState { value: "Quantum secure connection. Connected to %@, %@", comment: "" ), - tunnelInfo.exit.location.city, // TODO: Multihop - tunnelInfo.exit.location.country // TODO: Multihop + tunnelInfo.exit.location.city, + tunnelInfo.exit.location.country ) } else { String( @@ -198,8 +198,8 @@ extension TunnelState { value: "Secure connection. Connected to %@, %@", comment: "" ), - tunnelInfo.exit.location.city, // TODO: Multihop - tunnelInfo.exit.location.country // TODO: Multihop + tunnelInfo.exit.location.city, + tunnelInfo.exit.location.country ) } @@ -219,8 +219,8 @@ extension TunnelState { value: "Reconnecting to %@, %@", comment: "" ), - tunnelInfo.exit.location.city, // TODO: Multihop - tunnelInfo.exit.location.country // TODO: Multihop + tunnelInfo.exit.location.city, + tunnelInfo.exit.location.country ) case .waitingForConnectivity(.noConnection), .error: diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift index 2c5a6109b678..7d76a53bc404 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift @@ -83,24 +83,39 @@ enum TunnelState: Equatable, CustomStringConvertible { "pending reconnect after disconnect" case let .connecting(tunnelRelays, isPostQuantum): if let tunnelRelays { - "connecting \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelays.exit.hostname)" // TODO: Multihop + """ + connecting \(isPostQuantum ? "(PQ) " : "")\ + to \(tunnelRelays.exit.hostname)\ + \(tunnelRelays.entry.flatMap { " via \($0.hostname)" } ?? "") + """ } else { "connecting\(isPostQuantum ? " (PQ)" : ""), fetching relay" } case let .connected(tunnelRelays, isPostQuantum): - "connected \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelays.exit.hostname)" // TODO: Multihop + """ + connected \(isPostQuantum ? "(PQ) " : "")\ + to \(tunnelRelays.exit.hostname)\ + \(tunnelRelays.entry.flatMap { " via \($0.hostname)" } ?? "") + """ case let .disconnecting(actionAfterDisconnect): "disconnecting and then \(actionAfterDisconnect)" case .disconnected: "disconnected" case let .reconnecting(tunnelRelays, isPostQuantum): - "reconnecting \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelays.exit.hostname)" // TODO: Multihop + """ + reconnecting \(isPostQuantum ? "(PQ) " : "")\ + to \(tunnelRelays.exit.hostname)\ + \(tunnelRelays.entry.flatMap { " via \($0.hostname)" } ?? "") + """ case .waitingForConnectivity: "waiting for connectivity" case let .error(blockedStateReason): "error state: \(blockedStateReason)" case let .negotiatingPostQuantumKey(tunnelRelays, _): - "negotiating key with \(tunnelRelays.exit.hostname)" // TODO: Multihop + """ + negotiating key with exit relay: \(tunnelRelays.exit.hostname)\ + \(tunnelRelays.entry.flatMap { " via \($0.hostname)" } ?? "") + """ } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index c627f85fa360..8e7cac0676cb 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift @@ -139,7 +139,7 @@ final class TunnelControlView: UIView { connectButtonBlurView.isEnabled = model.enableButtons cityLabel.attributedText = attributedStringForLocation(string: model.city) countryLabel.attributedText = attributedStringForLocation(string: model.country) - connectionPanel.connectedRelayName = model.connectedRelayName + connectionPanel.connectedRelayName = model.connectedRelaysName connectionPanel.dataSource = model.connectionPanel updateSecureLabel(tunnelState: tunnelState) @@ -227,14 +227,15 @@ final class TunnelControlView: UIView { private func updateTunnelRelays(tunnelRelays: SelectedRelays?) { if let tunnelRelays { cityLabel.attributedText = attributedStringForLocation( - string: tunnelRelays.exit.location.city // TODO: Multihop + string: tunnelRelays.exit.location.city ) countryLabel.attributedText = attributedStringForLocation( - string: tunnelRelays.exit.location.country // TODO: Multihop + string: tunnelRelays.exit.location.country ) connectionPanel.isHidden = false - connectionPanel.connectedRelayName = tunnelRelays.exit.hostname // TODO: Multihop + connectionPanel.connectedRelayName = tunnelRelays.exit + .hostname + "\(tunnelRelays.entry.flatMap { " via \($0.hostname)" } ?? "")" } else { countryLabel.attributedText = attributedStringForLocation(string: " ") cityLabel.attributedText = attributedStringForLocation(string: " ") diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift index c0df319d2505..d1ce7b3a7a93 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift @@ -14,7 +14,7 @@ struct TunnelControlViewModel { let enableButtons: Bool let city: String let country: String - let connectedRelayName: String + let connectedRelaysName: String let outgoingConnectionInfo: OutgoingConnectionInfo? var connectionPanel: ConnectionPanelData? { @@ -29,7 +29,7 @@ struct TunnelControlViewModel { } return ConnectionPanelData( - inAddress: "\(tunnelRelays.exit.endpoint.ipv4Relay.ip)\(portAndTransport)", // TODO: Multihop + inAddress: "\(tunnelRelays.entry?.endpoint.ipv4Relay.ip ?? tunnelRelays.exit.endpoint.ipv4Relay.ip)\(portAndTransport)", outAddress: outgoingConnectionInfo?.outAddress ) } @@ -41,7 +41,7 @@ struct TunnelControlViewModel { enableButtons: true, city: "", country: "", - connectedRelayName: "", + connectedRelaysName: "", outgoingConnectionInfo: nil ) } @@ -53,7 +53,7 @@ struct TunnelControlViewModel { enableButtons: enableButtons, city: city, country: country, - connectedRelayName: connectedRelayName, + connectedRelaysName: connectedRelaysName, outgoingConnectionInfo: nil ) } @@ -65,7 +65,7 @@ struct TunnelControlViewModel { enableButtons: enableButtons, city: city, country: country, - connectedRelayName: connectedRelayName, + connectedRelaysName: connectedRelaysName, outgoingConnectionInfo: outgoingConnectionInfo ) } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift index b5d0dfab64bf..95e9260d207f 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift @@ -150,15 +150,15 @@ class TunnelViewController: UIViewController, RootContainment { case let .connecting(tunnelRelays, _): mapViewController.removeLocationMarker() contentView.setAnimatingActivity(true) - mapViewController.setCenter(tunnelRelays?.exit.location.geoCoordinate, animated: animated) // TODO: Multihop + mapViewController.setCenter(tunnelRelays?.exit.location.geoCoordinate, animated: animated) case let .reconnecting(tunnelRelays, _), let .negotiatingPostQuantumKey(tunnelRelays, _): mapViewController.removeLocationMarker() contentView.setAnimatingActivity(true) - mapViewController.setCenter(tunnelRelays.exit.location.geoCoordinate, animated: animated) // TODO: Multihop + mapViewController.setCenter(tunnelRelays.exit.location.geoCoordinate, animated: animated) case let .connected(tunnelRelays, _): - let center = tunnelRelays.exit.location.geoCoordinate // TODO: Multihop + let center = tunnelRelays.exit.location.geoCoordinate mapViewController.setCenter(center, animated: animated) { self.contentView.setAnimatingActivity(false) diff --git a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift index 637e47c89ebe..1eefa09816bf 100644 --- a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift @@ -25,7 +25,7 @@ final class PacketTunnelActorReducerTests: XCTestCase { keyPolicy: keyPolicy, networkReachability: .reachable, connectionAttemptCount: 0, - connectedEndpoint: selectedRelays.exit.endpoint, // TODO: Multihop + connectedEndpoint: selectedRelays.entry?.endpoint ?? selectedRelays.exit.endpoint, transportLayer: .udp, remotePort: 12345, isPostQuantum: false diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelSettingsStrategyTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelSettingsStrategyTests.swift new file mode 100644 index 000000000000..685c643d171d --- /dev/null +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelSettingsStrategyTests.swift @@ -0,0 +1,101 @@ +// +// TunnelSettingsStrategyTests.swift +// MullvadVPNTests +// +// Created by Mojgan on 2024-06-19. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// +import MullvadSettings +import MullvadTypes +import XCTest + +final class TunnelSettingsStrategyTests: XCTestCase { + func testConnectToNewRelayOnMultihopChanges() { + var currentSettings = LatestTunnelSettings() + TunnelSettingsUpdate.multihop(.off).apply(to: ¤tSettings) + + var updatedSettings = currentSettings + TunnelSettingsUpdate.multihop(.on).apply(to: &updatedSettings) + + let tunnelSettingsStrategy = TunnelSettingsStrategy() + XCTAssertTrue(tunnelSettingsStrategy.shouldReconnectToNewRelay( + oldSettings: currentSettings, + newSettings: updatedSettings + )) + } + + func testConnectToNewRelayOnRelaysConstraintChange() { + var currentSettings = LatestTunnelSettings() + TunnelSettingsUpdate.relayConstraints(RelayConstraints()).apply(to: ¤tSettings) + + var updatedSettings = currentSettings + TunnelSettingsUpdate.relayConstraints(RelayConstraints( + exitLocations: .only(UserSelectedRelays(locations: [.country("zz")])), + port: .only(9999), + filter: .only(.init(ownership: .rented, providers: .only(["foo", "bar"]))) + )).apply(to: &updatedSettings) + + let tunnelSettingsStrategy = TunnelSettingsStrategy() + XCTAssertTrue(tunnelSettingsStrategy.shouldReconnectToNewRelay( + oldSettings: currentSettings, + newSettings: updatedSettings + )) + } + + func testConnectToCurrentRelayOnDNSSettingsChange() { + let currentSettings = LatestTunnelSettings() + + var updatedSettings = currentSettings + var dnsSettings = DNSSettings() + dnsSettings.blockingOptions = [.blockAdvertising, .blockTracking] + dnsSettings.enableCustomDNS = true + TunnelSettingsUpdate.dnsSettings(dnsSettings).apply(to: &updatedSettings) + + let tunnelSettingsStrategy = TunnelSettingsStrategy() + XCTAssertFalse(tunnelSettingsStrategy.shouldReconnectToNewRelay( + oldSettings: currentSettings, + newSettings: updatedSettings + )) + } + + func testConnectToCurrentRelayOnQuantumResistanceChanges() { + var currentSettings = LatestTunnelSettings() + TunnelSettingsUpdate.quantumResistance(.off).apply(to: ¤tSettings) + + var updatedSettings = currentSettings + TunnelSettingsUpdate.quantumResistance(.on).apply(to: &updatedSettings) + + let tunnelSettingsStrategy = TunnelSettingsStrategy() + XCTAssertFalse(tunnelSettingsStrategy.shouldReconnectToNewRelay( + oldSettings: currentSettings, + newSettings: updatedSettings + )) + } + + func testConnectToCurrentRelayOnWireGuardObfuscationChange() { + var currentSettings = LatestTunnelSettings() + TunnelSettingsUpdate.obfuscation(WireGuardObfuscationSettings(state: .off, port: .port80)) + .apply(to: ¤tSettings) + + var updatedSettings = currentSettings + TunnelSettingsUpdate.obfuscation(WireGuardObfuscationSettings(state: .automatic, port: .automatic)) + .apply(to: &updatedSettings) + + let tunnelSettingsStrategy = TunnelSettingsStrategy() + XCTAssertFalse(tunnelSettingsStrategy.shouldReconnectToNewRelay( + oldSettings: currentSettings, + newSettings: updatedSettings + )) + } + + func testConnectToCurrentRelayWhenNothingChange() { + let currentSettings = LatestTunnelSettings() + let updatedSettings = currentSettings + + let tunnelSettingsStrategy = TunnelSettingsStrategy() + XCTAssertFalse(tunnelSettingsStrategy.shouldReconnectToNewRelay( + oldSettings: currentSettings, + newSettings: updatedSettings + )) + } +} diff --git a/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift b/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift index 3392e585ee1c..3351fcb554b7 100644 --- a/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift +++ b/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift @@ -45,6 +45,42 @@ struct WgAdapter: TunnelAdapterProtocol { } } + func startMultihop( + entryConfiguration: TunnelAdapterConfiguration? = nil, + exitConfiguration: TunnelAdapterConfiguration + ) async throws { + let exitConfiguration = exitConfiguration.asWgConfig + let entryConfiguration = entryConfiguration?.asWgConfig + + logger.info("\(exitConfiguration.peers)") + + if let entryConfiguration { + logger.info("\(entryConfiguration.peers)") + } + + do { + try await adapter.stop() + try await adapter.startMultihop( + entryConfiguration: entryConfiguration, + exitConfiguration: exitConfiguration + ) + } catch WireGuardAdapterError.invalidState { + try await adapter.startMultihop( + entryConfiguration: entryConfiguration, + exitConfiguration: exitConfiguration + ) + } + + let exitTunAddresses = exitConfiguration.interface.addresses.map { $0.address } + let entryTunAddresses = entryConfiguration?.interface.addresses.map { $0.address } ?? [] + let tunAddresses = exitTunAddresses + entryTunAddresses + + // TUN addresses can be empty when adapter is configured for blocked state. + if !tunAddresses.isEmpty { + logIfDeviceHasSameIP(than: tunAddresses) + } + } + func stop() async throws { try await adapter.stop() } diff --git a/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapter+Async.swift b/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapter+Async.swift index 1644597b9436..9c2322a71f0e 100644 --- a/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapter+Async.swift +++ b/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapter+Async.swift @@ -22,6 +22,18 @@ extension WireGuardAdapter { } } + func startMultihop(entryConfiguration: TunnelConfiguration?, exitConfiguration: TunnelConfiguration) async throws { + return try await withCheckedThrowingContinuation { continuation in + startMultihop(exitConfiguration: exitConfiguration, entryConfiguration: entryConfiguration) { 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 diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift index 30348546f173..a03f0ae4f98f 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift @@ -75,8 +75,8 @@ extension PacketTunnelActor { state = .connecting(connectionState) try? await tunnelAdapter.start(configuration: configurationBuilder.makeConfiguration()) - // Resume tunnel monitoring and use IPv4 gateway as a probe address. - tunnelMonitor.start(probeAddress: connectionState.selectedRelays.exit.endpoint.ipv4Gateway) // TODO: Multihop + // Resume tunnel monitoring and use exit IPv4 gateway as a probe address. + tunnelMonitor.start(probeAddress: connectionState.selectedRelays.exit.endpoint.ipv4Gateway) // Restart default path observer and notify the observer with the current path that might have changed while // path observer was paused. startDefaultPathObserver(notifyObserverWithCurrentPath: false) diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index b7a3bf204dfc..1b1731273989 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -20,11 +20,11 @@ import WireGuardKitTypes - Actor receives events for execution over the `EventChannel`. - Events are consumed in a detached task via for-await loop over the channel. Each event, once received, is executed in its entirety before the next - event is processed. See the implementation of `consumeEvents()` which is the central task dispatcher inside of actor. + event is processed. See the implementation of `consumeEvents()` which is the central task dispatcher inside of actor. - Most of calls that actor performs suspend for a very short amount of time. `EventChannel` proactively discards unwanted tasks as they arrive to prevent - future execution, such as repeating commands to reconnect are coalesced and all events prior to stop are discarded entirely as the outcome would be the - same anyway. + future execution, such as repeating commands to reconnect are coalesced and all events prior to stop are discarded entirely as the outcome would be the + same anyway. */ public actor PacketTunnelActor { var state: State = .initial { @@ -209,8 +209,8 @@ extension PacketTunnelActor { Reconnect tunnel to new relays. Enters error state on failure. - Parameters: - - nextRelay: next relays to connect to - - reason: reason for reconnect + - nextRelay: next relays to connect to + - reason: reason for reconnect */ private func reconnect(to nextRelays: NextRelays, reason: ActorReconnectReason) async { do { @@ -269,8 +269,8 @@ extension PacketTunnelActor { - Reactivate default path observation (disabled when configuring tunnel adapter) - Parameters: - - nextRelays: which relays should be selected next. - - reason: reason for reconnect + - nextRelays: which relays should be selected next. + - reason: reason for reconnect */ private func tryStartConnection( withSettings settings: Settings, @@ -289,16 +289,32 @@ extension PacketTunnelActor { state = .reconnecting(connectionState) } - let configurationBuilder = ConfigurationBuilder( + let entryConfiguration: TunnelAdapterConfiguration? = if connectionState.selectedRelays.entry != nil { + try ConfigurationBuilder( + privateKey: activeKey, + interfaceAddresses: settings.interfaceAddresses, + dns: settings.dnsServers, + endpoint: connectionState.connectedEndpoint, + allowedIPs: [ + IPAddressRange(from: "\(connectionState.selectedRelays.exit.endpoint.ipv4Relay.ip)/32")!, + ] + ).makeConfiguration() + } else { + nil + } + + let exitConfiguration = try ConfigurationBuilder( privateKey: activeKey, interfaceAddresses: settings.interfaceAddresses, dns: settings.dnsServers, - endpoint: connectionState.connectedEndpoint, + endpoint: (entryConfiguration != nil) + ? connectionState.selectedRelays.exit.endpoint + : connectionState.connectedEndpoint, allowedIPs: [ IPAddressRange(from: "0.0.0.0/0")!, IPAddressRange(from: "::/0")!, ] - ) + ).makeConfiguration() /* Stop default path observer while updating WireGuard configuration since it will call the system method @@ -313,19 +329,22 @@ extension PacketTunnelActor { startDefaultPathObserver(notifyObserverWithCurrentPath: true) } - try await tunnelAdapter.start(configuration: configurationBuilder.makeConfiguration()) + try await tunnelAdapter.startMultihop( + entryConfiguration: entryConfiguration, + exitConfiguration: exitConfiguration + ) // Resume tunnel monitoring and use IPv4 gateway as a probe address. - tunnelMonitor.start(probeAddress: connectionState.selectedRelays.exit.endpoint.ipv4Gateway) // TODO: Multihop + tunnelMonitor.start(probeAddress: connectionState.selectedRelays.exit.endpoint.ipv4Gateway) } /** Derive `ConnectionState` from current `state` updating it with new relays and settings. - Parameters: - - nextRelays: relay preference that should be used when selecting next relays. - - settings: current settings - - reason: reason for reconnect + - nextRelays: relay preference that should be used when selecting next relays. + - settings: current settings + - reason: reason for reconnect - Returns: New connection state or `nil` if current state is at or past `.disconnecting` phase. */ @@ -348,8 +367,6 @@ extension PacketTunnelActor { } switch state { - case .initial: - break // Handle PQ PSK separately as it doesn't interfere with either the `.connecting` or `.reconnecting` states. case var .negotiatingPostQuantumKey(connectionState, _): if reason == .connectionLoss { @@ -359,8 +376,12 @@ extension PacketTunnelActor { connectionState.selectedRelays, connectionState.connectionAttemptCount ) + let connectedRelay = selectedRelays.entry ?? selectedRelays.exit connectionState.selectedRelays = selectedRelays connectionState.relayConstraints = settings.relayConstraints + connectionState.connectedEndpoint = connectedRelay.endpoint + connectionState.remotePort = connectedRelay.endpoint.ipv4Relay.port + return connectionState case var .connecting(connectionState), var .reconnecting(connectionState): if reason == .connectionLoss { @@ -372,31 +393,37 @@ extension PacketTunnelActor { connectionState.selectedRelays, connectionState.connectionAttemptCount ) + let connectedRelay = selectedRelays.entry ?? selectedRelays.exit connectionState.selectedRelays = selectedRelays connectionState.relayConstraints = settings.relayConstraints connectionState.currentKey = settings.privateKey + connectionState.connectedEndpoint = connectedRelay.endpoint + connectionState.remotePort = connectedRelay.endpoint.ipv4Relay.port return connectionState case let .error(blockedState): keyPolicy = blockedState.keyPolicy lastKeyRotation = blockedState.lastKeyRotation networkReachability = blockedState.networkReachability + fallthrough + case .initial: + let selectedRelays = try callRelaySelector(nil, 0) + let connectedRelay = selectedRelays.entry ?? selectedRelays.exit + return State.ConnectionData( + selectedRelays: selectedRelays, + relayConstraints: settings.relayConstraints, + currentKey: settings.privateKey, + keyPolicy: keyPolicy, + networkReachability: networkReachability, + connectionAttemptCount: 0, + lastKeyRotation: lastKeyRotation, + connectedEndpoint: connectedRelay.endpoint, + transportLayer: .udp, + remotePort: connectedRelay.endpoint.ipv4Relay.port, + isPostQuantum: settings.quantumResistance.isEnabled + ) case .disconnecting, .disconnected: return nil } - let selectedRelays = try callRelaySelector(nil, 0) - return State.ConnectionData( - selectedRelays: selectedRelays, - relayConstraints: settings.relayConstraints, - currentKey: settings.privateKey, - keyPolicy: keyPolicy, - networkReachability: networkReachability, - connectionAttemptCount: 0, - lastKeyRotation: lastKeyRotation, - connectedEndpoint: selectedRelays.exit.endpoint, // TODO: Multihop - transportLayer: .udp, - remotePort: selectedRelays.exit.endpoint.ipv4Relay.port, // TODO: Multihop - isPostQuantum: settings.quantumResistance.isEnabled - ) } internal func activeKey(from state: State.ConnectionData, in settings: Settings) -> PrivateKey { @@ -417,9 +444,9 @@ extension PacketTunnelActor { else { return nil } let obfuscatedEndpoint = protocolObfuscator.obfuscate( - connectionState.selectedRelays.exit.endpoint, // TODO: Multihop + connectionState.connectedEndpoint, settings: settings, - retryAttempts: connectionState.selectedRelays.exit.retryAttempts // TODO: Multihop + retryAttempts: connectionState.selectedRelays.retryAttempt ) let transportLayer = protocolObfuscator.transportLayer.map { $0 } ?? .udp @@ -442,10 +469,10 @@ extension PacketTunnelActor { Select next relay to connect to based on `NextRelays` and other input parameters. - Parameters: - - nextRelays: next relays to connect to. - - relayConstraints: relay constraints. - - currentRelays: currently selected relays. - - connectionAttemptCount: number of failed connection attempts so far. + - nextRelays: next relays to connect to. + - relayConstraints: relay constraints. + - currentRelays: currently selected relays. + - connectionAttemptCount: number of failed connection attempts so far. - Returns: selector result that contains the credentials of the next relays that the tunnel should connect to. */ diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift index fd731a32e150..2579a7de39d5 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift @@ -53,7 +53,7 @@ extension PacketTunnelActor { case .random: return "reconnect(random, \(stopTunnelMonitor))" case let .preSelected(selectedRelays): - return "reconnect(\(selectedRelays.exit.hostname), \(stopTunnelMonitor))" // TODO: Multihop + return "reconnect(\(selectedRelays.exit.hostname)\(selectedRelays.entry.flatMap { " via \($0.hostname)" } ?? ""), \(stopTunnelMonitor))" } case let .error(reason): return "error(\(reason))" diff --git a/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift index ea798075d24b..e07ba4664e1e 100644 --- a/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift +++ b/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift @@ -17,6 +17,12 @@ public protocol TunnelAdapterProtocol { /// Start tunnel adapter or update active configuration. func start(configuration: TunnelAdapterConfiguration) async throws + /// Start tunnel adapter or update active configuration. + func startMultihop( + entryConfiguration: TunnelAdapterConfiguration?, + exitConfiguration: TunnelAdapterConfiguration + ) async throws + /// Stop tunnel adapter with the given configuration. func stop() async throws } diff --git a/ios/PacketTunnelCore/Actor/StartOptions.swift b/ios/PacketTunnelCore/Actor/StartOptions.swift index 4c8ad7587891..9af92fe34ce2 100644 --- a/ios/PacketTunnelCore/Actor/StartOptions.swift +++ b/ios/PacketTunnelCore/Actor/StartOptions.swift @@ -27,7 +27,8 @@ public struct StartOptions { public func logFormat() -> String { var s = "Start the tunnel via \(launchSource)" if let selectedRelays { - s += ", connect to \(selectedRelays.exit.hostname)" // TODO: Multihop + s += ", connect to \(selectedRelays.exit.hostname)" + s += selectedRelays.entry.flatMap { " via \($0.hostname)" } ?? "" } s += "." return s diff --git a/ios/PacketTunnelCore/Actor/State+Extensions.swift b/ios/PacketTunnelCore/Actor/State+Extensions.swift index 69d6579a9db9..f7d8cbfae796 100644 --- a/ios/PacketTunnelCore/Actor/State+Extensions.swift +++ b/ios/PacketTunnelCore/Actor/State+Extensions.swift @@ -47,20 +47,19 @@ extension State { func logFormat() -> String { switch self { case let .connecting(connState), let .connected(connState), let .reconnecting(connState): - let hostname = connState.selectedRelays.exit.hostname // TODO: Multihop - - return """ - \(name) to \(hostname), \ + """ + \(name) to \(connState.selectedRelays.entry.flatMap { "entry location: \($0.hostname) " } ?? ""),\ + exit location: \(connState.selectedRelays.exit.hostname), \ key: \(connState.keyPolicy.logFormat()), \ net: \(connState.networkReachability), \ attempt: \(connState.connectionAttemptCount) """ case let .error(blockedState): - return "\(name): \(blockedState.reason)" + "\(name): \(blockedState.reason)" case .initial, .disconnecting, .disconnected, .negotiatingPostQuantumKey: - return name + name } } diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift index df82b1128650..ee7e00ded31b 100644 --- a/ios/PacketTunnelCore/Actor/State.swift +++ b/ios/PacketTunnelCore/Actor/State.swift @@ -141,12 +141,12 @@ extension State { } /// The actual endpoint fed to WireGuard, can be a local endpoint if obfuscation is used. - public let connectedEndpoint: MullvadEndpoint + public var connectedEndpoint: MullvadEndpoint /// Via which transport protocol was the connection made to the relay public let transportLayer: TransportLayer /// The remote port that was chosen to connect to `connectedEndpoint` - public let remotePort: UInt16 + public var remotePort: UInt16 /// True if post-quantum key exchange is enabled public let isPostQuantum: Bool diff --git a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift index 680b438f8395..8e2a42fd886e 100644 --- a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift +++ b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift @@ -100,9 +100,9 @@ final class AppMessageHandlerTests: XCTestCase { exit: SelectedRelay( endpoint: match.endpoint, hostname: match.relay.hostname, - location: match.location, - retryAttempts: 0 - ) + location: match.location + ), + retryAttempt: 0 ) _ = try? await appMessageHandler.handleAppMessage( diff --git a/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift b/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift index 4b7040f7566f..79b75a002d70 100644 --- a/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift +++ b/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift @@ -11,6 +11,11 @@ import PacketTunnelCore /// Dummy tunnel adapter that does nothing and reports no errors. class TunnelAdapterDummy: TunnelAdapterProtocol { + func startMultihop( + entryConfiguration: TunnelAdapterConfiguration?, + exitConfiguration: TunnelAdapterConfiguration + ) async throws {} + func start(configuration: TunnelAdapterConfiguration) async throws {} func stop() async throws {}