diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2021-09-21 12:42:16 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2021-09-29 13:28:11 +0200 |
| commit | fa5faf8a20a401be2a5c308ed037429b346ceeba (patch) | |
| tree | 7ccf06e7bdd1f7c119a12a0774826347162d151e /ios | |
| parent | bdfc8ae5599595c57ab0042fafede23efb4070b4 (diff) | |
| download | mullvadvpn-fa5faf8a20a401be2a5c308ed037429b346ceeba.tar.xz mullvadvpn-fa5faf8a20a401be2a5c308ed037429b346ceeba.zip | |
TunnelManager: refactor
Diffstat (limited to 'ios')
25 files changed, 1941 insertions, 1754 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index cfe857d837..0e62c96269 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -14,30 +14,23 @@ 580EE22424B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */; }; 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */; }; 5815039724D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */; }; - 5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */; }; 5815039D24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039C24D6ECE600C9C50E /* TextFileOutputStream.swift */; }; - 5815039E24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039C24D6ECE600C9C50E /* TextFileOutputStream.swift */; }; - 581503A024D6F01E00C9C50E /* LogRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039324D6EB7200C9C50E /* LogRotation.swift */; }; 581503A124D6F01F00C9C50E /* LogRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039324D6EB7200C9C50E /* LogRotation.swift */; }; 581503A324D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A224D6F1EC00C9C50E /* ChainedError+Logger.swift */; }; - 581503A424D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A224D6F1EC00C9C50E /* ChainedError+Logger.swift */; }; 581503A624D6F4AE00C9C50E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; }; - 581503A724D6F4AE00C9C50E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; }; 581CBCEE229826FD00727D7F /* StaticTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581CBCED229826FD00727D7F /* StaticTableViewDataSource.swift */; }; 581FC4FA2695ACE100AA97BA /* Account.strings in Resources */ = {isa = PBXBuildFile; fileRef = 581FC4F82695ACE100AA97BA /* Account.strings */; }; 5820674926E63EC900655B05 /* Promise+BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */; }; 5820674E26E6510200655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; }; 5820675026E6514100655B05 /* HTTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674F26E6514100655B05 /* HTTP.swift */; }; - 5820675526E6528200655B05 /* RelayCacheError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87926B024F900B8C587 /* RelayCacheError.swift */; }; - 5820675626E6528A00655B05 /* RESTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88626B0277200B8C587 /* RESTError.swift */; }; - 5820675726E652A600655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; }; - 5820675826E652AF00655B05 /* RelayCacheIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87C26B0254000B8C587 /* RelayCacheIO.swift */; }; - 5820675926E652BE00655B05 /* RESTCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88926B027A300B8C587 /* RESTCoding.swift */; }; 5820675B26E6576800655B05 /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; }; - 5820675C26E6576800655B05 /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; }; 5820675E26E6839900655B05 /* PresentAlertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675D26E6839900655B05 /* PresentAlertOperation.swift */; }; + 5820676026E75A4D00655B05 /* Promise+Delay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675F26E75A4D00655B05 /* Promise+Delay.swift */; }; + 5820676126E75A4D00655B05 /* Promise+Delay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675F26E75A4D00655B05 /* Promise+Delay.swift */; }; 5820676226E75D8500655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; }; - 5823FA5126CA690600283BF8 /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; }; + 5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerError.swift */; }; + 5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */; }; + 5823FA5626CE4A2B00283BF8 /* AnyTunnelObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA5526CE4A2B00283BF8 /* AnyTunnelObserver.swift */; }; 58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */; }; 58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB025124117005D0BB5 /* CustomTextField.swift */; }; 58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB2251241B3005D0BB5 /* CustomTextView.swift */; }; @@ -52,13 +45,58 @@ 582CFEE726945FC30072883A /* AppStoreSubscriptions.strings in Resources */ = {isa = PBXBuildFile; fileRef = 582CFEE526945FC30072883A /* AppStoreSubscriptions.strings */; }; 582CFEEA269463B80072883A /* Settings.strings in Resources */ = {isa = PBXBuildFile; fileRef = 582CFEE8269463B80072883A /* Settings.strings */; }; 582E021A26F49D2E0031BCD8 /* AnyRelayCacheObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865726EA213300F188BC /* AnyRelayCacheObserver.swift */; }; - 582E021B26F49D3B0031BCD8 /* RelayCacheObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865926EA214400F188BC /* RelayCacheObserver.swift */; }; 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; }; 583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DA21325FA4B5C00318683 /* LocationDataSource.swift */; }; 5840250122B1124600E4CFEC /* IPAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */; }; - 5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */; }; 5840250422B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; }; - 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; }; + 58432B9726F9D74700F97148 /* RelayCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */; }; + 58432B9A26F9D78E00F97148 /* TunnelConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */; }; + 58432B9B26F9D7B700F97148 /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; + 58432B9C26F9D7C400F97148 /* ChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840B12464491D0044E708 /* ChainedError.swift */; }; + 58432B9E26F9D82000F97148 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; + 58432B9F26F9D85800F97148 /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; }; + 58432BA026F9D86600F97148 /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; + 58432BA126F9D87200F97148 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; }; + 58432BA226F9D87500F97148 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; }; + 58432BA326F9D88D00F97148 /* IPAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */; }; + 58432BA426F9D89C00F97148 /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; }; + 58432BA526F9D8A500F97148 /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; + 58432BA726F9D8CD00F97148 /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; + 58432BA826F9D8D100F97148 /* AnyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336C26D2BE7500CC316B /* AnyResult.swift */; }; + 58432BA926F9D8D400F97148 /* Promise+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337426D2BEC400CC316B /* Promise+Optional.swift */; }; + 58432BAA26F9D8D800F97148 /* Promise+ReceiveOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */; }; + 58432BAB26F9D8E600F97148 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; + 58432BAC26F9D8ED00F97148 /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; }; + 58432BAD26F9D8F800F97148 /* Promise+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1338026D2BF5C00CC316B /* Promise+Result.swift */; }; + 58432BAE26F9D90400F97148 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; }; + 58432BAF26F9D90A00F97148 /* Locking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA692D23E99EFF009DC256 /* Locking.swift */; }; + 58432BB026F9D91300F97148 /* TunnelSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettings.swift */; }; + 58432BB126F9DA0A00F97148 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; }; + 58432BB226F9DA3F00F97148 /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; }; + 58432BB326F9DA4200F97148 /* RelayCacheIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87C26B0254000B8C587 /* RelayCacheIO.swift */; }; + 58432BB426F9DA4E00F97148 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; + 58432BB526F9DA6700F97148 /* ChainedError+Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A224D6F1EC00C9C50E /* ChainedError+Logger.swift */; }; + 58432BB626F9DA6F00F97148 /* CachedRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87626B024A600B8C587 /* CachedRelays.swift */; }; + 58432BB726F9DA8700F97148 /* RelayCacheError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87926B024F900B8C587 /* RelayCacheError.swift */; }; + 58432BB826F9DA9500F97148 /* RESTCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88926B027A300B8C587 /* RESTCoding.swift */; }; + 58432BB926F9DAB300F97148 /* TunnelIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* TunnelIPC.swift */; }; + 58432BBA26F9DAB700F97148 /* TunnelIPCCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88E26B031E200B8C587 /* TunnelIPCCoding.swift */; }; + 58432BBB26F9DABE00F97148 /* TunnelIPCRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89226B0323E00B8C587 /* TunnelIPCRequest.swift */; }; + 58432BBC26F9DAC000F97148 /* TunnelIPCResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89526B0328000B8C587 /* TunnelIPCResponse.swift */; }; + 58432BBD26F9DACC00F97148 /* PacketTunnelOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */; }; + 58432BBE26F9DADE00F97148 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; }; + 58432BBF26F9DAE600F97148 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; + 58432BC026F9DAF100F97148 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; + 58432BC126F9DAF700F97148 /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; }; + 58432BC226F9DAFC00F97148 /* KeychainClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */; }; + 58432BC326F9DB0000F97148 /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; }; + 58432BC426F9DB0200F97148 /* KeychainReturn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */; }; + 58432BC526F9DB0F00F97148 /* RESTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88626B0277200B8C587 /* RESTError.swift */; }; + 58432BC626F9DB2700F97148 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; }; + 58432BC726F9DB2D00F97148 /* TextFileOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039C24D6ECE600C9C50E /* TextFileOutputStream.swift */; }; + 58432BC826F9DB3700F97148 /* LogRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039324D6EB7200C9C50E /* LogRotation.swift */; }; + 58432BC926F9DB3F00F97148 /* CustomFormatLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */; }; + 58432BCA26F9DB4500F97148 /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; }; 584592612639B4A200EF967F /* ConsentContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* ConsentContentView.swift */; }; 5846226526E0D9630035F7C2 /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; }; 5846226726E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */; }; @@ -75,15 +113,16 @@ 584789BF264D4A2A000E45FB /* new_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B7264D4A2A000E45FB /* new_le_root_cert.cer */; }; 584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; }; 584789EC2652A1A2000E45FB /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 584789EB2652A1A2000E45FB /* Logging */; }; + 584C041626F9D64E00BD45A6 /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; }; + 584C041726F9D65000BD45A6 /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; }; + 584C041826F9D66C00BD45A6 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; }; + 584C041926F9D66D00BD45A6 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; }; + 584C041A26F9D6F900BD45A6 /* RelayCacheObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865926EA214400F188BC /* RelayCacheObserver.swift */; }; 584E96BC240FD4DA00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; - 584E96BD240FD4DA00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; 584E96BE240FD4DB00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; - 5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; }; 5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; }; 5850368C25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; }; - 5850368D25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; }; 58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; - 58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; 5857F23024C843ED00CF6F47 /* ChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840B12464491D0044E708 /* ChainedError.swift */; }; 5857F23424C8443700CF6F47 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; }; 5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */; }; @@ -94,33 +133,25 @@ 585834FC24D2BC9500A8AF56 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 585834FB24D2BC9500A8AF56 /* Logging */; }; 585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585CA70E25F8C44600B47C62 /* UIMetrics.swift */; }; 585DA87726B024A600B8C587 /* CachedRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87626B024A600B8C587 /* CachedRelays.swift */; }; - 585DA87826B024A900B8C587 /* CachedRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87626B024A600B8C587 /* CachedRelays.swift */; }; 585DA87A26B024F900B8C587 /* RelayCacheError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87926B024F900B8C587 /* RelayCacheError.swift */; }; 585DA87D26B0254000B8C587 /* RelayCacheIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87C26B0254000B8C587 /* RelayCacheIO.swift */; }; 585DA88426B0270700B8C587 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; }; - 585DA88526B0270700B8C587 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; }; 585DA88726B0277200B8C587 /* RESTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88626B0277200B8C587 /* RESTError.swift */; }; 585DA88A26B027A300B8C587 /* RESTCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88926B027A300B8C587 /* RESTCoding.swift */; }; - 585DA88F26B031E200B8C587 /* TunnelIPCCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88E26B031E200B8C587 /* TunnelIPCCoding.swift */; }; 585DA89126B0322700B8C587 /* TunnelIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* TunnelIPC.swift */; }; 585DA89326B0323E00B8C587 /* TunnelIPCRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89226B0323E00B8C587 /* TunnelIPCRequest.swift */; }; - 585DA89426B0323E00B8C587 /* TunnelIPCRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89226B0323E00B8C587 /* TunnelIPCRequest.swift */; }; 585DA89626B0328000B8C587 /* TunnelIPCResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89526B0328000B8C587 /* TunnelIPCResponse.swift */; }; - 585DA89726B0328000B8C587 /* TunnelIPCResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89526B0328000B8C587 /* TunnelIPCResponse.swift */; }; + 585DA89926B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */; }; 585DA89B26B146B300B8C587 /* TunnelIPCCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88E26B031E200B8C587 /* TunnelIPCCoding.swift */; }; 585DA8A326B14E0D00B8C587 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; }; + 585DA8A526B14EE000B8C587 /* TunnelConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */; }; 585DA8A626B14F5100B8C587 /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; }; - 585DA8AF26B9492500B8C587 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; }; 5860392726D91B8400554C79 /* PromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE526D23C3D001CB97C /* PromiseTests.swift */; }; 5860392926DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; - 5860392A26DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; 5860392B26DCEE6300554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; }; 5868585524054096000B8131 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868585424054096000B8131 /* AppButton.swift */; }; 5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */; }; - 586A17C926F8AC8B0028D8D7 /* PacketTunnelOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586A17C826F8AC8B0028D8D7 /* PacketTunnelOptions.swift */; }; - 586A17CA26F8AC8B0028D8D7 /* PacketTunnelOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586A17C826F8AC8B0028D8D7 /* PacketTunnelOptions.swift */; }; - 586AA296234B696B00502875 /* WireguardAssociatedAddresses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */; }; 586ADD4723FC13F400CE9E87 /* countries.geo.json in Resources */ = {isa = PBXBuildFile; fileRef = 586ADD4523FC13F400CE9E87 /* countries.geo.json */; }; 5871FB8325498CA20051A0A4 /* Swizzle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB8225498CA20051A0A4 /* Swizzle.swift */; }; 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */; }; @@ -130,21 +161,18 @@ 587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587425C02299833500CA2045 /* RootContainerViewController.swift */; }; 5875960726F36B3A00BF6711 /* TunnelIPCError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5875960626F36B3A00BF6711 /* TunnelIPCError.swift */; }; 5875960A26F371FC00BF6711 /* TunnelIPCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5875960926F371FC00BF6711 /* TunnelIPCSession.swift */; }; - 5875960B26F3723000BF6711 /* TunnelIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* TunnelIPC.swift */; }; 5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5877152F23981F7B001F8237 /* WireguardKeysViewController.swift */; }; 58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; - 58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; - 58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; 5878BA1426DD0B01004147D7 /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; }; 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */; }; 587AD7C623421D7000E93A53 /* TunnelSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettings.swift */; }; - 587AD7C723421D8600E93A53 /* TunnelSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettings.swift */; }; 587AD7CA2342283900E93A53 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C92342283900E93A53 /* Account.swift */; }; 587B7536266528A200DEF7E9 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B7535266528A200DEF7E9 /* NotificationManager.swift */; }; 587B753B2666467500DEF7E9 /* NotificationBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B753A2666467500DEF7E9 /* NotificationBannerView.swift */; }; 587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B753C2666468F00DEF7E9 /* NotificationController.swift */; }; 587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B753E2668E5A700DEF7E9 /* NotificationContainerView.swift */; }; 587B75412668FD7800DEF7E9 /* AccountExpiryNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B75402668FD7700DEF7E9 /* AccountExpiryNotificationProvider.swift */; }; + 587C575326D2615F005EF767 /* PacketTunnelOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */; }; 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */; }; 5883A09E266A5AF7003EFFCB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 587B7543266922BF00DEF7E9 /* Localizable.strings */; }; 58871D1E25D535A3002297FA /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58871D1D25D535A3002297FA /* WireGuardKit */; }; @@ -152,7 +180,6 @@ 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD82227B11080051EB06 /* SelectLocationCell.swift */; }; 5888AD87227B17950051EB06 /* SelectLocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD86227B17950051EB06 /* SelectLocationViewController.swift */; }; 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; }; - 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */; }; 58907D9524D17B4E00CFC3F5 /* DisconnectSplitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */; }; 5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; }; 5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */; }; @@ -176,11 +203,7 @@ 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */; }; 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */; }; 58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; - 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; 58AEEF6B2344A46200C9BBD5 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; }; - 58AEEF6C2344A49D00C9BBD5 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; }; - 58B06F7026F37C3900D415AD /* TunnelConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */; }; - 58B06F7126F37C3B00D415AD /* TunnelConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */; }; 58B06F7226F37C3B00D415AD /* TunnelConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */; }; 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26F3237434D00073B10E /* RelaySelectorTests.swift */; }; 58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; @@ -190,8 +213,7 @@ 58B43C1925F77DB60002C8C3 /* ConnectMainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B43C1825F77DB60002C8C3 /* ConnectMainContentView.swift */; }; 58B67B482602079E008EF58E /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */; }; - 58B93A1826C54D7E00A55733 /* Locking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA692D23E99EFF009DC256 /* Locking.swift */; }; - 58B93A2526C683B300A55733 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; }; + 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B93A1226C3F13600A55733 /* TunnelState.swift */; }; 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B993B02608A34500BA7811 /* LoginContentView.swift */; }; 58B9EB132488ED2100095626 /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB122488ED2100095626 /* AlertPresenter.swift */; }; 58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB142489139B00095626 /* DisplayChainedError.swift */; }; @@ -200,10 +222,7 @@ 58BA791B2578F092006FAEA0 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58BA791A2578F092006FAEA0 /* WireGuardKit */; }; 58BA7947257901A5006FAEA0 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58BA7946257901A5006FAEA0 /* WireGuardKit */; }; 58BF345E26F09F3C002A6CAA /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */; }; - 58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */; }; 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; - 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; - 58C3478B26C1094F0060838B /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; }; 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */; }; 58CAF4EF26025954007C5886 /* SimulatorTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */; }; 58CB0EE024B86751001EF0D8 /* RESTClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CB0EDF24B86751001EF0D8 /* RESTClient.swift */; }; @@ -220,25 +239,16 @@ 58CE5E81224146470008646E /* PacketTunnel.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 58CE5E79224146470008646E /* PacketTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 58D0C79E23F1CEBA00FE9BA7 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C79D23F1CEBA00FE9BA7 /* SnapshotHelper.swift */; }; 58D0C7A223F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */; }; - 58D67A0A26D7AE3300557C3C /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; }; 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */; }; 58E1336926D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; - 58E1336A26D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; 58E1336B26D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; 58E1336D26D2BE7500CC316B /* AnyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336C26D2BE7500CC316B /* AnyResult.swift */; }; - 58E1336E26D2BE7500CC316B /* AnyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336C26D2BE7500CC316B /* AnyResult.swift */; }; 58E1336F26D2BE7500CC316B /* AnyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336C26D2BE7500CC316B /* AnyResult.swift */; }; - 58E1337126D2BE9C00CC316B /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; }; - 58E1337226D2BE9C00CC316B /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; }; - 58E1337326D2BE9C00CC316B /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; }; 58E1337526D2BEC400CC316B /* Promise+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337426D2BEC400CC316B /* Promise+Optional.swift */; }; - 58E1337626D2BEC400CC316B /* Promise+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337426D2BEC400CC316B /* Promise+Optional.swift */; }; 58E1337726D2BEC400CC316B /* Promise+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337426D2BEC400CC316B /* Promise+Optional.swift */; }; 58E1337926D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */; }; - 58E1337A26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */; }; 58E1337B26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */; }; 58E1338126D2BF5C00CC316B /* Promise+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1338026D2BF5C00CC316B /* Promise+Result.swift */; }; - 58E1338226D2BF5C00CC316B /* Promise+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1338026D2BF5C00CC316B /* Promise+Result.swift */; }; 58E1338326D2BF5C00CC316B /* Promise+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1338026D2BF5C00CC316B /* Promise+Result.swift */; }; 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */; }; 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */; }; @@ -267,22 +277,16 @@ 58F61F4F2692F21C00DCFC2B /* WireguardKeys.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F61F4D2692F21C00DCFC2B /* WireguardKeys.strings */; }; 58F7CA882692E34000FC59FD /* WireguardKeysContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F7CA872692E34000FC59FD /* WireguardKeysContentView.swift */; }; 58F840B22464491D0044E708 /* ChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840B12464491D0044E708 /* ChainedError.swift */; }; - 58F840B32464491D0044E708 /* ChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840B12464491D0044E708 /* ChainedError.swift */; }; 58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */; }; 58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; }; - 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; }; 58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; 58FAEDF7245088E100CB0F5B /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; - 58FAEDF8245088E100CB0F5B /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; 58FAEDFD24533A5500CB0F5B /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; }; 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */; }; 58FAEE0124533A9C00CB0F5B /* KeychainClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */; }; - 58FAEE0224533ABB00CB0F5B /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; }; - 58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */; }; - 58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */; }; 58FB865526E8BF3100F188BC /* AppStorePaymentManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865426E8BF3100F188BC /* AppStorePaymentManagerError.swift */; }; 58FB865E26EA284E00F188BC /* LogFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865D26EA284E00F188BC /* LogFormatting.swift */; }; - 58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865D26EA284E00F188BC /* LogFormatting.swift */; }; + 58FB866126EB678000F188BC /* TunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB866026EB677F00F188BC /* TunnelInfo.swift */; }; 58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */; }; 58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; }; 58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */; }; @@ -354,7 +358,11 @@ 5820674F26E6514100655B05 /* HTTP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTP.swift; sourceTree = "<group>"; }; 5820675A26E6576800655B05 /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = "<group>"; }; 5820675D26E6839900655B05 /* PresentAlertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAlertOperation.swift; sourceTree = "<group>"; }; + 5820675F26E75A4D00655B05 /* Promise+Delay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Delay.swift"; sourceTree = "<group>"; }; + 5820676326E771DB00655B05 /* TunnelManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerError.swift; sourceTree = "<group>"; }; 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogHandler.swift; sourceTree = "<group>"; }; + 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObserver.swift; sourceTree = "<group>"; }; + 5823FA5526CE4A2B00283BF8 /* AnyTunnelObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyTunnelObserver.swift; sourceTree = "<group>"; }; 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportViewController.swift; sourceTree = "<group>"; }; 58293FB025124117005D0BB5 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; }; 58293FB2251241B3005D0BB5 /* CustomTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextView.swift; sourceTree = "<group>"; }; @@ -405,7 +413,6 @@ 5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MullvadVPN.entitlements; sourceTree = "<group>"; }; 5868585424054096000B8131 /* AppButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = "<group>"; }; 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSplitViewController.swift; sourceTree = "<group>"; }; - 586A17C826F8AC8B0028D8D7 /* PacketTunnelOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PacketTunnelOptions.swift; sourceTree = "<group>"; }; 586ADD4523FC13F400CE9E87 /* countries.geo.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = countries.geo.json; sourceTree = "<group>"; }; 5871FB8225498CA20051A0A4 /* Swizzle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Swizzle.swift; sourceTree = "<group>"; }; 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLog.swift; sourceTree = "<group>"; }; @@ -427,6 +434,7 @@ 587B753E2668E5A700DEF7E9 /* NotificationContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContainerView.swift; sourceTree = "<group>"; }; 587B75402668FD7700DEF7E9 /* AccountExpiryNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryNotificationProvider.swift; sourceTree = "<group>"; }; 587B7544266922BF00DEF7E9 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; + 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelOptions.swift; sourceTree = "<group>"; }; 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Helpers.swift"; sourceTree = "<group>"; }; 5888AD82227B11080051EB06 /* SelectLocationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationCell.swift; sourceTree = "<group>"; }; 5888AD86227B17950051EB06 /* SelectLocationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationViewController.swift; sourceTree = "<group>"; }; @@ -453,6 +461,7 @@ 58B0A2A4238EE67E00BC001D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 58B43C1825F77DB60002C8C3 /* ConnectMainContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectMainContentView.swift; sourceTree = "<group>"; }; 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardAssociatedAddresses.swift; sourceTree = "<group>"; }; + 58B93A1226C3F13600A55733 /* TunnelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelState.swift; sourceTree = "<group>"; }; 58B993B02608A34500BA7811 /* LoginContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginContentView.swift; sourceTree = "<group>"; }; 58B9EB122488ED2100095626 /* AlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresenter.swift; sourceTree = "<group>"; }; 58B9EB142489139B00095626 /* DisplayChainedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayChainedError.swift; sourceTree = "<group>"; }; @@ -527,6 +536,7 @@ 58FB865726EA213300F188BC /* AnyRelayCacheObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyRelayCacheObserver.swift; sourceTree = "<group>"; }; 58FB865926EA214400F188BC /* RelayCacheObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheObserver.swift; sourceTree = "<group>"; }; 58FB865D26EA284E00F188BC /* LogFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogFormatting.swift; sourceTree = "<group>"; }; + 58FB866026EB677F00F188BC /* TunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelInfo.swift; sourceTree = "<group>"; }; 58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreReceipt.swift; sourceTree = "<group>"; }; 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Formatting.swift"; sourceTree = "<group>"; }; 58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInterfaceInteractionRestriction.swift; sourceTree = "<group>"; }; @@ -607,6 +617,20 @@ path = Logging; sourceTree = "<group>"; }; + 5823FA5726CE4A4100283BF8 /* TunnelManager */ = { + isa = PBXGroup; + children = ( + 5823FA5526CE4A2B00283BF8 /* AnyTunnelObserver.swift */, + 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */, + 58FB866026EB677F00F188BC /* TunnelInfo.swift */, + 5835B7CB233B76CB0096D79F /* TunnelManager.swift */, + 5820676326E771DB00655B05 /* TunnelManagerError.swift */, + 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */, + 58B93A1226C3F13600A55733 /* TunnelState.swift */, + ); + path = TunnelManager; + sourceTree = "<group>"; + }; 582CFEE1269448160072883A /* Localizations */ = { isa = PBXGroup; children = ( @@ -692,14 +716,6 @@ path = TunnelIPC; sourceTree = "<group>"; }; - 586A17C726F8AC8B0028D8D7 /* TunnelManager */ = { - isa = PBXGroup; - children = ( - 586A17C826F8AC8B0028D8D7 /* PacketTunnelOptions.swift */, - ); - path = TunnelManager; - sourceTree = "<group>"; - }; 586ADD4323FC13AD00CE9E87 /* GeoJSON */ = { isa = PBXGroup; children = ( @@ -852,8 +868,7 @@ 5871FB8225498CA20051A0A4 /* Swizzle.swift */, 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */, 585DA88D26B031D100B8C587 /* TunnelIPC */, - 586A17C726F8AC8B0028D8D7 /* TunnelManager */, - 5835B7CB233B76CB0096D79F /* TunnelManager.swift */, + 5823FA5726CE4A4100283BF8 /* TunnelManager */, 587AD7C523421D7000E93A53 /* TunnelSettings.swift */, 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */, 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */, @@ -898,6 +913,7 @@ 58E1337026D2BE9C00CC316B /* AnyOptional.swift */, 58E1337426D2BEC400CC316B /* Promise+Optional.swift */, 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */, + 5820675F26E75A4D00655B05 /* Promise+Delay.swift */, 58E1338026D2BF5C00CC316B /* Promise+Result.swift */, 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */, 5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */, @@ -1181,6 +1197,7 @@ 58E1336F26D2BE7500CC316B /* AnyResult.swift in Sources */, 5896AE80246ACE79005B36CB /* KeychainClass.swift in Sources */, 582AE3132440CA2700E6733A /* AccountTokenInput.swift in Sources */, + 5820676126E75A4D00655B05 /* Promise+Delay.swift in Sources */, 58CAF4EF26025954007C5886 /* SimulatorTunnelProvider.swift in Sources */, 58B0A2AA238EE6A900BC001D /* RelaySelector.swift in Sources */, 58E1337726D2BEC400CC316B /* Promise+Optional.swift in Sources */, @@ -1194,18 +1211,19 @@ 58BF345E26F09F3C002A6CAA /* ExclusivityController.swift in Sources */, 58E1338326D2BF5C00CC316B /* Promise+Result.swift in Sources */, 585DA8A626B14F5100B8C587 /* SSLPinningURLSessionDelegate.swift in Sources */, + 584C041626F9D64E00BD45A6 /* AnyOptional.swift in Sources */, + 584C041926F9D66D00BD45A6 /* Promise.swift in Sources */, 58B0A2AC238EE6D500BC001D /* IPAddress+Codable.swift in Sources */, 58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */, 5846226826E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */, 5860392B26DCEE6300554C79 /* PromiseCompletion.swift in Sources */, 58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */, 5860392726D91B8400554C79 /* PromiseTests.swift in Sources */, - 58E1337326D2BE9C00CC316B /* AnyOptional.swift in Sources */, 5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */, 58A94AE626D23C3D001CB97C /* PromiseTests.swift in Sources */, - 58C3478B26C1094F0060838B /* Promise.swift in Sources */, 5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */, 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */, + 585DA8A526B14EE000B8C587 /* TunnelConnectionInfo.swift in Sources */, 5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */, 58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */, 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */, @@ -1229,17 +1247,19 @@ 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, 5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */, 587B75412668FD7800DEF7E9 /* AccountExpiryNotificationProvider.swift in Sources */, + 585DA89926B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */, 58BA692E23E99EFF009DC256 /* Locking.swift in Sources */, 5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */, 5840250122B1124600E4CFEC /* IPAddress+Codable.swift in Sources */, 5857F24724C882D700CF6F47 /* SelectLocationNavigationController.swift in Sources */, 5846227126E229F20035F7C2 /* AppStoreSubscription.swift in Sources */, - 58E1337126D2BE9C00CC316B /* AnyOptional.swift in Sources */, - 5846226526E0D9630035F7C2 /* ProductsRequestOperation.swift in Sources */, 5820675B26E6576800655B05 /* RelayCache.swift in Sources */, + 5846226526E0D9630035F7C2 /* ProductsRequestOperation.swift in Sources */, 585DA87D26B0254000B8C587 /* RelayCacheIO.swift in Sources */, 58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */, + 587C575326D2615F005EF767 /* PacketTunnelOptions.swift in Sources */, 58E1336D26D2BE7500CC316B /* AnyResult.swift in Sources */, + 584C041A26F9D6F900BD45A6 /* RelayCacheObserver.swift in Sources */, 587B753B2666467500DEF7E9 /* NotificationBannerView.swift in Sources */, 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */, 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */, @@ -1247,12 +1267,11 @@ 582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */, 582E021A26F49D2E0031BCD8 /* AnyRelayCacheObserver.swift in Sources */, 582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */, + 58432B9726F9D74700F97148 /* RelayCacheTracker.swift in Sources */, 585DA88426B0270700B8C587 /* ServerRelaysResponse.swift in Sources */, 5875960726F36B3A00BF6711 /* TunnelIPCError.swift in Sources */, 58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */, 58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */, - 58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */, - 58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */, 582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */, 584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */, 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */, @@ -1279,7 +1298,9 @@ 5871FB8325498CA20051A0A4 /* Swizzle.swift in Sources */, 58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */, 58907D9524D17B4E00CFC3F5 /* DisconnectSplitButton.swift in Sources */, + 5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */, 5888AD87227B17950051EB06 /* SelectLocationViewController.swift in Sources */, + 5820676026E75A4D00655B05 /* Promise+Delay.swift in Sources */, 58E1336926D2BE3700CC316B /* PromiseObserver.swift in Sources */, 58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */, 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */, @@ -1287,7 +1308,6 @@ 58FAEE0124533A9C00CB0F5B /* KeychainClass.swift in Sources */, 58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */, 58AEEF6B2344A46200C9BBD5 /* TunnelSettingsManager.swift in Sources */, - 58B06F7026F37C3900D415AD /* TunnelConnectionInfo.swift in Sources */, 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */, 58FAEDFD24533A5500CB0F5B /* KeychainMatchLimit.swift in Sources */, 5846227526E22A350035F7C2 /* AnyAppStorePaymentObserver.swift in Sources */, @@ -1303,10 +1323,10 @@ 584E96BC240FD4DA00D3334F /* Location.swift in Sources */, 581503A124D6F01F00C9C50E /* LogRotation.swift in Sources */, 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */, + 5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */, 5846226726E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */, 5850368C25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */, 58B67B482602079E008EF58E /* RelaySelector.swift in Sources */, - 586A17C926F8AC8B0028D8D7 /* PacketTunnelOptions.swift in Sources */, 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */, 583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */, 5878BA1426DD0B01004147D7 /* OSLogHandler.swift in Sources */, @@ -1325,9 +1345,13 @@ 58E1337926D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */, 58CE5E66224146200008646E /* LoginViewController.swift in Sources */, 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */, + 5823FA5626CE4A2B00283BF8 /* AnyTunnelObserver.swift in Sources */, + 584C041726F9D65000BD45A6 /* AnyOptional.swift in Sources */, 58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */, 5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */, + 58FB866126EB678000F188BC /* TunnelInfo.swift in Sources */, 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */, + 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */, 58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */, 5815039724D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */, 5820675E26E6839900655B05 /* PresentAlertOperation.swift in Sources */, @@ -1338,7 +1362,6 @@ 5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */, 58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */, 581503A624D6F4AE00C9C50E /* Logging.swift in Sources */, - 582E021B26F49D3B0031BCD8 /* RelayCacheObserver.swift in Sources */, 58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */, 58FB865526E8BF3100F188BC /* AppStorePaymentManagerError.swift in Sources */, 58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */, @@ -1346,8 +1369,8 @@ 58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */, 589AB4F7227B64450039131E /* BasicTableViewCell.swift in Sources */, 58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */, - 585DA8AF26B9492500B8C587 /* Promise.swift in Sources */, 587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */, + 584C041826F9D66C00BD45A6 /* Promise.swift in Sources */, 5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */, 58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */, 584592612639B4A200EF967F /* ConsentContentView.swift in Sources */, @@ -1382,62 +1405,54 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */, - 58B93A1826C54D7E00A55733 /* Locking.swift in Sources */, - 58FAEE0224533ABB00CB0F5B /* KeychainMatchLimit.swift in Sources */, - 5860392A26DCE7AB00554C79 /* PromiseCompletion.swift in Sources */, - 58FAEE0224533ABB00CB0F5B /* KeychainMatchLimit.swift in Sources */, - 58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */, - 585DA89726B0328000B8C587 /* TunnelIPCResponse.swift in Sources */, - 58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */, - 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, - 5850368D25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */, - 5820675826E652AF00655B05 /* RelayCacheIO.swift in Sources */, - 5820675726E652A600655B05 /* REST.swift in Sources */, - 58E1337A26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */, - 58B93A2526C683B300A55733 /* Promise.swift in Sources */, - 586A17CA26F8AC8B0028D8D7 /* PacketTunnelOptions.swift in Sources */, - 585DA88F26B031E200B8C587 /* TunnelIPCCoding.swift in Sources */, - 58E1337A26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */, - 58B93A2526C683B300A55733 /* Promise.swift in Sources */, - 585DA89426B0323E00B8C587 /* TunnelIPCRequest.swift in Sources */, - 587AD7C723421D8600E93A53 /* TunnelSettings.swift in Sources */, - 5875960B26F3723000BF6711 /* TunnelIPC.swift in Sources */, - 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */, - 5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */, - 58E1337226D2BE9C00CC316B /* AnyOptional.swift in Sources */, - 5820675C26E6576800655B05 /* RelayCache.swift in Sources */, + 58432BBD26F9DACC00F97148 /* PacketTunnelOptions.swift in Sources */, + 58432B9B26F9D7B700F97148 /* IPEndpoint.swift in Sources */, + 58432BAC26F9D8ED00F97148 /* AnyOptional.swift in Sources */, + 58432BC626F9DB2700F97148 /* Logging.swift in Sources */, + 58432BA926F9D8D400F97148 /* Promise+Optional.swift in Sources */, + 58432BB526F9DA6700F97148 /* ChainedError+Logger.swift in Sources */, + 58432BA726F9D8CD00F97148 /* PromiseObserver.swift in Sources */, + 58432BB726F9DA8700F97148 /* RelayCacheError.swift in Sources */, + 58432BC326F9DB0000F97148 /* KeychainMatchLimit.swift in Sources */, + 58432BB226F9DA3F00F97148 /* RelayCache.swift in Sources */, + 58432BC526F9DB0F00F97148 /* RESTError.swift in Sources */, + 58432BAD26F9D8F800F97148 /* Promise+Result.swift in Sources */, 58CE5E7C224146470008646E /* PacketTunnelProvider.swift in Sources */, - 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */, - 586AA296234B696B00502875 /* WireguardAssociatedAddresses.swift in Sources */, - 58E1337226D2BE9C00CC316B /* AnyOptional.swift in Sources */, - 58E1338226D2BF5C00CC316B /* Promise+Result.swift in Sources */, - 585DA88526B0270700B8C587 /* ServerRelaysResponse.swift in Sources */, - 581503A724D6F4AE00C9C50E /* Logging.swift in Sources */, - 58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */, - 58AEEF6C2344A49D00C9BBD5 /* TunnelSettingsManager.swift in Sources */, - 58E1336E26D2BE7500CC316B /* AnyResult.swift in Sources */, - 581503A424D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */, - 58E1336A26D2BE3700CC316B /* PromiseObserver.swift in Sources */, - 5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */, - 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */, - 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */, - 5815039E24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */, - 585DA87826B024A900B8C587 /* CachedRelays.swift in Sources */, - 58B06F7126F37C3B00D415AD /* TunnelConnectionInfo.swift in Sources */, - 584E96BD240FD4DA00D3334F /* Location.swift in Sources */, - 58FAEDF8245088E100CB0F5B /* Keychain.swift in Sources */, - 58F840B32464491D0044E708 /* ChainedError.swift in Sources */, - 58D67A0A26D7AE3300557C3C /* OSLogHandler.swift in Sources */, - 5820675626E6528A00655B05 /* RESTError.swift in Sources */, - 5820675526E6528200655B05 /* RelayCacheError.swift in Sources */, - 58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */, - 58E1337626D2BEC400CC316B /* Promise+Optional.swift in Sources */, - 58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */, - 581503A024D6F01E00C9C50E /* LogRotation.swift in Sources */, - 58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */, - 5823FA5126CA690600283BF8 /* OSLogHandler.swift in Sources */, - 5820675926E652BE00655B05 /* RESTCoding.swift in Sources */, + 58432BB026F9D91300F97148 /* TunnelSettings.swift in Sources */, + 58432BA526F9D8A500F97148 /* RelayConstraints.swift in Sources */, + 58432BAE26F9D90400F97148 /* Promise.swift in Sources */, + 58432BA026F9D86600F97148 /* RelaySelector.swift in Sources */, + 58432BA126F9D87200F97148 /* ServerRelaysResponse.swift in Sources */, + 58432BA326F9D88D00F97148 /* IPAddress+Codable.swift in Sources */, + 58432BC726F9DB2D00F97148 /* TextFileOutputStream.swift in Sources */, + 58432BC226F9DAFC00F97148 /* KeychainClass.swift in Sources */, + 58432BBC26F9DAC000F97148 /* TunnelIPCResponse.swift in Sources */, + 58432BC926F9DB3F00F97148 /* CustomFormatLogHandler.swift in Sources */, + 58432BB426F9DA4E00F97148 /* ApplicationConfiguration.swift in Sources */, + 58432BAF26F9D90A00F97148 /* Locking.swift in Sources */, + 58432BB126F9DA0A00F97148 /* PrivateKeyWithMetadata.swift in Sources */, + 58432BC026F9DAF100F97148 /* KeychainError.swift in Sources */, + 58432BAA26F9D8D800F97148 /* Promise+ReceiveOn.swift in Sources */, + 58432BA226F9D87500F97148 /* REST.swift in Sources */, + 58432B9F26F9D85800F97148 /* IPAddressRange+Codable.swift in Sources */, + 58432BA426F9D89C00F97148 /* MullvadEndpoint.swift in Sources */, + 58432BBA26F9DAB700F97148 /* TunnelIPCCoding.swift in Sources */, + 58432BBF26F9DAE600F97148 /* Keychain.swift in Sources */, + 58432BB926F9DAB300F97148 /* TunnelIPC.swift in Sources */, + 58432BAB26F9D8E600F97148 /* PromiseCompletion.swift in Sources */, + 58432B9E26F9D82000F97148 /* Location.swift in Sources */, + 58432BBE26F9DADE00F97148 /* TunnelSettingsManager.swift in Sources */, + 58432BB326F9DA4200F97148 /* RelayCacheIO.swift in Sources */, + 58432B9A26F9D78E00F97148 /* TunnelConnectionInfo.swift in Sources */, + 58432BBB26F9DABE00F97148 /* TunnelIPCRequest.swift in Sources */, + 58432BB626F9DA6F00F97148 /* CachedRelays.swift in Sources */, + 58432BC826F9DB3700F97148 /* LogRotation.swift in Sources */, + 58432BC126F9DAF700F97148 /* KeychainAttributes.swift in Sources */, + 58432BB826F9DA9500F97148 /* RESTCoding.swift in Sources */, + 58432BA826F9D8D100F97148 /* AnyResult.swift in Sources */, + 58432B9C26F9D7C400F97148 /* ChainedError.swift in Sources */, + 58432BC426F9DB0200F97148 /* KeychainReturn.swift in Sources */, + 58432BCA26F9DB4500F97148 /* OSLogHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift index 5d3f603d1a..14a9e21326 100644 --- a/ios/MullvadVPN/Account.swift +++ b/ios/MullvadVPN/Account.swift @@ -7,7 +7,6 @@ // import Foundation -import NetworkExtension import StoreKit import Logging @@ -98,13 +97,7 @@ class Account { } } - private enum ExclusivityCategory { - case exclusive - } - - private let rest = MullvadRest() - private let operationQueue = OperationQueue() - private lazy var exclusivityController = ExclusivityController<ExclusivityCategory>(operationQueue: operationQueue) + private let dispatchQueue = DispatchQueue(label: "AccountQueue") var isLoggedIn: Bool { return token != nil @@ -115,147 +108,111 @@ class Account { UserDefaults.standard.set(true, forKey: UserDefaultsKeys.isAgreedToTermsOfService.rawValue) } - func loginWithNewAccount(completionHandler: @escaping (Result<AccountResponse, Error>) -> Void) { - let operation = rest.createAccount().operation(payload: EmptyPayload()) - - operation.addDidFinishBlockObserver(queue: .main) { (operation, result) in - switch result { - case .success(let response): - self.setupTunnel(accountToken: response.token, expiry: response.expires) { (result) in - if case .success = result { + func loginWithNewAccount() -> Result<REST.AccountResponse, Account.Error>.Promise { + return REST.Client.shared.createAccount() + .mapError { error in + return Error.createAccount(error) + } + .receive(on: .main) + .mapThen { response in + return self.setupTunnel(accountToken: response.token, expiry: response.expires) + .map { _ in self.observerList.forEach { (observer) in observer.account(self, didLoginWithToken: response.token, expiry: response.expires) } + return response } - completionHandler(result.map { response }) - } - - case .failure(let error): - completionHandler(.failure(.createAccount(error))) } - } - - exclusivityController.addOperation(operation, categories: [.exclusive]) + .block(on: dispatchQueue) + .receive(on: .main) } /// Perform the login and save the account token along with expiry (if available) to the /// application preferences. - func login(with accountToken: String, completionHandler: @escaping (Result<AccountResponse, Error>) -> Void) { - let operation = rest.getAccountExpiry() - .operation(payload: .init(token: accountToken, payload: EmptyPayload())) - - operation.addDidFinishBlockObserver(queue: .main) { (operation, result) in - switch result { - case .success(let response): - self.setupTunnel(accountToken: response.token, expiry: response.expires) { (result) in - if case .success = result { + func login(with accountToken: String) -> Result<REST.AccountResponse, Account.Error>.Promise { + return REST.Client.shared.getAccountExpiry(token: accountToken) + .mapError { error in + return Account.Error.verifyAccount(error) + } + .mapThen { response in + return self.setupTunnel(accountToken: response.token, expiry: response.expires) + .map { _ in self.observerList.forEach { (observer) in observer.account(self, didLoginWithToken: response.token, expiry: response.expires) } + return response } - completionHandler(result.map { response }) - } - - case .failure(let error): - completionHandler(.failure(.verifyAccount(error))) } - } - - exclusivityController.addOperation(operation, categories: [.exclusive]) + .block(on: dispatchQueue) + .receive(on: .main) } /// Perform the logout by erasing the account token and expiry from the application preferences. - func logout(completionHandler: @escaping (Result<(), Error>) -> Void) { - let operation = ResultOperation<(), Error> { (finish) in - TunnelManager.shared.unsetAccount { (result) in - DispatchQueue.main.async { - switch result { - case .success: - self.removeFromPreferences() - self.observerList.forEach { (observer) in - observer.accountDidLogout(self) - } - - finish(.success(())) - - case .failure(let error): - finish(.failure(.tunnelConfiguration(error))) - } - } + func logout() -> Result<(), Account.Error>.Promise { + return TunnelManager.shared.unsetAccount() + .mapError { error in + return Account.Error.tunnelConfiguration(error) } - } - - operation.addDidFinishBlockObserver(queue: .main) { (operation, result) in - completionHandler(result) - } - - exclusivityController.addOperation(operation, categories: [.exclusive]) - } - - /// Forget that user was logged in, but do not attempt to unset account in `TunnelManager`. - /// This function is used in cases where the tunnel or tunnel settings are corrupt. - func forget(completionHandler: @escaping () -> Void) { - let operation = AsyncBlockOperation { (finish) in - DispatchQueue.main.async { + .receive(on: .main) + .onSuccess { _ in self.removeFromPreferences() self.observerList.forEach { (observer) in observer.accountDidLogout(self) } - finish() } - } + .block(on: dispatchQueue) + .receive(on: .main) + } - operation.addDidFinishBlockObserver(queue: .main) { (operation) in - completionHandler() + /// Forget that user was logged in, but do not attempt to unset account in `TunnelManager`. + /// This function is used in cases where the tunnel or tunnel settings are corrupt. + func forget() -> Promise<Void> { + return Promise<Void> { resolver in + self.removeFromPreferences() + self.observerList.forEach { (observer) in + observer.accountDidLogout(self) + } + resolver.resolve(value: ()) } - - exclusivityController.addOperation(operation, categories: [.exclusive]) + .schedule(on: .main) + .block(on: dispatchQueue) + .receive(on: .main) } func updateAccountExpiry() { - let makeRequest = ResultOperation { () -> TokenPayload<EmptyPayload>? in - return self.token.flatMap { (token) in - return TokenPayload(token: token, payload: EmptyPayload()) + Promise<String?>.deferred { self.token } + .mapThen(defaultValue: nil) { token in + return REST.Client.shared.getAccountExpiry(token: token) + .onFailure { error in + self.logger.error(chainedError: error, message: "Failed to update account expiry") + } + .success() } - } - - let sendRequest = rest.getAccountExpiry() - .operation(payload: nil) - .injectResult(from: makeRequest) + .schedule(on: .main) + .block(on: dispatchQueue) + .receive(on: .main) + .observe { completion in + guard let response = completion.flattenUnwrappedValue else { return } - sendRequest.addDidFinishBlockObserver(queue: .main) { (operation, result) in - switch result { - case .success(let response): if self.expiry != response.expires { self.expiry = response.expires self.observerList.forEach { (observer) in observer.account(self, didUpdateExpiry: response.expires) } } - - case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to update account expiry") } - } - - exclusivityController.addOperations([makeRequest, sendRequest], categories: [.exclusive]) } - private func setupTunnel(accountToken: String, expiry: Date, completionHandler: @escaping (Result<(), Error>) -> Void) { - TunnelManager.shared.setAccount(accountToken: accountToken) { (managerResult) in - DispatchQueue.main.async { - switch managerResult { - case .success: - self.token = accountToken - self.expiry = expiry - - completionHandler(.success(())) - - case .failure(let error): - completionHandler(.failure(.tunnelConfiguration(error))) - } + private func setupTunnel(accountToken: String, expiry: Date) -> Result<(), Error>.Promise { + return TunnelManager.shared.setAccount(accountToken: accountToken) + .receive(on: .main) + .mapError { error in + return Error.tunnelConfiguration(error) + } + .onSuccess { _ in + self.token = accountToken + self.expiry = expiry } - } } private func removeFromPreferences() { @@ -286,23 +243,17 @@ extension Account: AppStorePaymentObserver { // no-op } - func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, didFinishWithResponse response: CreateApplePaymentResponse) { - let newExpiry = response.newExpiry + func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, didFinishWithResponse response: REST.CreateApplePaymentResponse) { + dispatchQueue.async { + let newExpiry = response.newExpiry - let operation = AsyncBlockOperation { (finish) in - DispatchQueue.main.async { - // Make sure that payment corresponds to the active account token - if self.token == accountToken, self.expiry != newExpiry { - self.expiry = newExpiry - self.observerList.forEach { (observer) in - observer.account(self, didUpdateExpiry: newExpiry) - } + // Make sure that payment corresponds to the active account token + if self.token == accountToken, self.expiry != newExpiry { + self.expiry = newExpiry + self.observerList.forEach { (observer) in + observer.account(self, didUpdateExpiry: newExpiry) } - - finish() } } - - exclusivityController.addOperation(operation, categories: [.exclusive]) } } diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index fcbc2542ae..250333111c 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -22,10 +22,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private let simulatorTunnelProvider = SimulatorTunnelProviderHost() #endif - #if DEBUG - private let packetTunnelLogForwarder = LogStreamer<UTF8>(fileURLs: [ApplicationConfiguration.packetTunnelLogFileURL!]) - #endif - private var rootContainer: RootContainerViewController? private var splitViewController: CustomSplitViewController? private var selectLocationViewController: SelectLocationViewController? @@ -40,7 +36,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } private var relayConstraints: RelayConstraints? - private let alertPresenter = AlertPresenter() private let notificationManager = NotificationManager() @@ -52,13 +47,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { self.logger = Logger(label: "AppDelegate") - #if DEBUG - let stdoutStream = TextFileOutputStream.standardOutputStream() - packetTunnelLogForwarder.start { (str) in - stdoutStream.write("\(str)\n") - } - #endif - #if targetEnvironment(simulator) // Configure mock tunnel provider on simulator SimulatorTunnelProvider.shared.delegate = simulatorTunnelProvider @@ -94,36 +82,33 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } // Load tunnels - self.logger?.debug("Load tunnels") - TunnelManager.shared.loadTunnel(accountToken: Account.shared.token) { (result) in - DispatchQueue.main.async { - switch result { - case .success: - self.logger?.debug("Loaded tunnels") - self.relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints - self.didFinishInitialization() - - case .failure(let error): - self.logger?.error(chainedError: error, message: "Failed to load tunnels") + TunnelManager.shared.loadTunnel(accountToken: Account.shared.token) + .receive(on: .main) + .onSuccess { _ in + self.relayConstraints = TunnelManager.shared.tunnelInfo?.tunnelSettings.relayConstraints + self.didFinishInitialization() + } + .onFailure { error in + self.logger?.error(chainedError: error, message: "Failed to load tunnels") - switch error { - case .loadAllVPNConfigurations(_), .removeInconsistentVPNConfiguration(_): - // TODO: avoid throwing fatal error and show the problem report UI instead. - fatalError(error.displayChain(message: "Failed to load tunnels")) + switch error { + case .loadAllVPNConfigurations(_), .removeInconsistentVPNConfiguration(_): + // TODO: avoid throwing fatal error and show the problem report UI instead. + fatalError(error.displayChain(message: "Failed to load tunnels")) - case .migrateTunnelSettings(_), .readTunnelSettings(_): - // Forget that user was logged in since tunnel settings are likely corrupt - // or missing. - Account.shared.forget { + case .migrateTunnelSettings(_), .readTunnelSettings(_): + // Forget that user was logged in since tunnel settings are likely corrupt + // or missing. + Account.shared.forget() + .observe { _ in self.didFinishInitialization() } - default: - fatalError("Unexpected error coming from loadTunnel()") - } + default: + fatalError("Unexpected error coming from loadTunnel()") } } - } + .observe { _ in } // Show the window self.window?.makeKeyAndVisible() @@ -132,15 +117,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func applicationDidBecomeActive(_ application: UIApplication) { - TunnelManager.shared.refreshTunnelState(completionHandler: nil) - // Start periodic relays updates RelayCache.Tracker.shared.startPeriodicUpdates() + + // Start periodic private key rotation + TunnelManager.shared.startPeriodicPrivateKeyRotation() } func applicationWillResignActive(_ application: UIApplication) { // Stop periodic relays updates RelayCache.Tracker.shared.stopPeriodicUpdates() + + // Stop periodic private key rotation + TunnelManager.shared.stopPeriodicPrivateKeyRotation() + } } // MARK: - Private @@ -395,9 +385,11 @@ extension AppDelegate: RootContainerViewControllerDelegate { switch TunnelManager.shared.tunnelState { case .connected, .connecting, .reconnecting: - reconnectTunnel() + TunnelManager.shared.reconnectTunnel() case .disconnecting, .disconnected: - connectTunnel() + TunnelManager.shared.startTunnel() + case .pendingReconnect: + break } return true } @@ -449,7 +441,7 @@ extension AppDelegate: LoginViewControllerDelegate { // Move the settings button back into header bar self.rootContainer?.removeSettingsButtonFromPresentationContainer() - self.relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints + self.relayConstraints = TunnelManager.shared.tunnelInfo?.tunnelSettings.relayConstraints self.selectLocationViewController?.setSelectedRelayLocation(relayConstraints?.location.value, animated: false, scrollPosition: .middle) switch UIDevice.current.userInterfaceIdiom { @@ -526,85 +518,9 @@ extension AppDelegate: ConnectViewControllerDelegate { self.selectLocationViewController = contentController } - func connectViewControllerShouldConnectTunnel(_ controller: ConnectViewController) { - connectTunnel() - } - - func connectViewControllerShouldDisconnectTunnel(_ controller: ConnectViewController) { - disconnectTunnel() - } - - func connectViewControllerShouldReconnectTunnel(_ controller: ConnectViewController) { - reconnectTunnel() - } - @objc private func handleDismissSelectLocationController(_ sender: Any) { self.selectLocationViewController?.dismiss(animated: true) } - - private func connectTunnel() { - TunnelManager.shared.startTunnel { (result) in - DispatchQueue.main.async { - switch result { - case .success: - self.logger?.debug("Connected VPN tunnel") - - case .failure(let error): - self.logger?.error(chainedError: error, message: "Failed to start the VPN tunnel") - self.presentTunnelError(error, alertTitle: NSLocalizedString( - "START_VPN_TUNNEL_ERROR_ALERT_TITLE", - tableName: "AppDelegate", - value: "Failed to start the VPN tunnel", - comment: "" - )) - } - } - } - } - - private func disconnectTunnel() { - TunnelManager.shared.stopTunnel { (result) in - switch result { - case .success: - self.logger?.debug("Disconnected VPN tunnel") - - case .failure(let error): - self.logger?.error(chainedError: error, message: "Failed to stop the VPN tunnel") - self.presentTunnelError(error, alertTitle: NSLocalizedString( - "STOP_VPN_TUNNEL_ERROR_ALERT_TITLE", - tableName: "AppDelegate", - value: "Failed to stop the VPN tunnel", - comment: "" - )) - } - } - } - - private func reconnectTunnel() { - TunnelManager.shared.reconnectTunnel { - self.logger?.debug("Re-connected VPN tunnel") - } - } - - private func presentTunnelError(_ error: TunnelManager.Error, alertTitle: String) { - let alertController = UIAlertController( - title: alertTitle, - message: error.errorChainDescription, - preferredStyle: .alert - ) - alertController.addAction( - UIAlertAction( - title: NSLocalizedString( - "TUNNEL_ERROR_ALERT_OK_BUTTON", - tableName: "AppDelegate", - comment: "Dismiss button in tunnel error alert." - ), - style: .cancel - ) - ) - - self.alertPresenter.enqueue(alertController, presentingController: self.rootContainer!) - } } // MARK: - SelectLocationViewControllerDelegate @@ -628,22 +544,22 @@ extension AppDelegate: SelectLocationViewControllerDelegate { private func selectLocationControllerDidSelectRelayLocation(_ relayLocation: RelayLocation) { let relayConstraints = RelayConstraints(location: .only(relayLocation)) - TunnelManager.shared.setRelayConstraints(relayConstraints) { [weak self] (result) in - guard let self = self else { return } + TunnelManager.shared.setRelayConstraints(relayConstraints) + .receive(on: .main) + .observe { completion in + guard let result = completion.unwrappedValue else { return } - DispatchQueue.main.async { self.relayConstraints = relayConstraints switch result { case .success: self.logger?.debug("Updated relay constraints: \(relayConstraints)") - self.connectTunnel() + TunnelManager.shared.startTunnel() case .failure(let error): self.logger?.error(chainedError: error, message: "Failed to update relay constraints") } } - } } } diff --git a/ios/MullvadVPN/ConnectMainContentView.swift b/ios/MullvadVPN/ConnectMainContentView.swift index 3be22d95bd..7c03cb7b0d 100644 --- a/ios/MullvadVPN/ConnectMainContentView.swift +++ b/ios/MullvadVPN/ConnectMainContentView.swift @@ -13,6 +13,7 @@ class ConnectMainContentView: UIView { enum ActionButton { case connect case disconnect + case cancel case selectLocation } @@ -60,6 +61,13 @@ class ConnectMainContentView: UIView { return button }() + lazy var cancelButton: AppButton = { + let button = AppButton(style: .translucentDanger) + button.accessibilityIdentifier = "CancelButton" + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + lazy var selectLocationButton: AppButton = { let button = AppButton(style: .translucentNeutral) button.accessibilityIdentifier = "SelectLocationButton" @@ -71,6 +79,10 @@ class ConnectMainContentView: UIView { return TranslucentButtonBlurView(button: selectLocationButton) }() + lazy var cancelButtonBlurView: TranslucentButtonBlurView = { + return TranslucentButtonBlurView(button: cancelButton) + }() + let splitDisconnectButton: DisconnectSplitButton = { let button = DisconnectSplitButton() button.primaryButton.accessibilityIdentifier = "DisconnectButton" @@ -220,6 +232,8 @@ class ConnectMainContentView: UIView { return connectButton case .disconnect: return splitDisconnectButton + case .cancel: + return cancelButtonBlurView case .selectLocation: return selectLocationBlurView } diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift index f010eec162..77d6b61395 100644 --- a/ios/MullvadVPN/ConnectViewController.swift +++ b/ios/MullvadVPN/ConnectViewController.swift @@ -20,9 +20,6 @@ class CustomOverlayRenderer: MKOverlayRenderer { protocol ConnectViewControllerDelegate: AnyObject { func connectViewControllerShouldShowSelectLocationPicker(_ controller: ConnectViewController) - func connectViewControllerShouldConnectTunnel(_ controller: ConnectViewController) - func connectViewControllerShouldDisconnectTunnel(_ controller: ConnectViewController) - func connectViewControllerShouldReconnectTunnel(_ controller: ConnectViewController) } class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainment, TunnelObserver, AccountObserver { @@ -54,7 +51,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen case .connecting, .reconnecting, .connected: return HeaderBarPresentation(style: .secured, showsDivider: false) - case .disconnecting, .disconnected: + case .disconnecting, .disconnected, .pendingReconnect: return HeaderBarPresentation(style: .unsecured, showsDivider: false) } } @@ -81,6 +78,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen super.viewDidLoad() mainContentView.connectButton.addTarget(self, action: #selector(handleConnect(_:)), for: .touchUpInside) + mainContentView.cancelButton.addTarget(self, action: #selector(handleDisconnect(_:)), for: .touchUpInside) mainContentView.splitDisconnectButton.primaryButton.addTarget(self, action: #selector(handleDisconnect(_:)), for: .touchUpInside) mainContentView.splitDisconnectButton.secondaryButton.addTarget(self, action: #selector(handleReconnect(_:)), for: .touchUpInside) @@ -144,13 +142,15 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen // MARK: - TunnelObserver - func tunnelStateDidChange(tunnelState: TunnelState) { - DispatchQueue.main.async { - self.tunnelState = tunnelState - } + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) { + // no-op + } + + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { + self.tunnelState = tunnelState } - func tunnelSettingsDidChange(tunnelSettings: TunnelSettings?) { + func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { // no-op } @@ -160,9 +160,30 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen mainContentView.secureLabel.text = tunnelState.localizedTitleForSecureLabel.uppercased() mainContentView.secureLabel.textColor = tunnelState.textColorForSecureLabel - mainContentView.connectButton.setTitle(tunnelState.localizedTitleForConnectButton, for: .normal) + mainContentView.connectButton.setTitle( + NSLocalizedString( + "CONNECT_BUTTON_TITLE", + tableName: "Main", + value: "Secure connection", + comment: "" + ), for: .normal + ) mainContentView.selectLocationButton.setTitle(tunnelState.localizedTitleForSelectLocationButton, for: .normal) - mainContentView.splitDisconnectButton.primaryButton.setTitle(tunnelState.localizedTitleForDisconnectButton, for: .normal) + mainContentView.cancelButton.setTitle( + NSLocalizedString( + "CANCEL_BUTTON_TITLE", + tableName: "Main", + value: "Cancel", + comment: "" + ), for: .normal) + mainContentView.splitDisconnectButton.primaryButton.setTitle( + NSLocalizedString( + "DISCONNECT_BUTTON_TITLE", + tableName: "Main", + value: "Disconnect", + comment: "" + ), for: .normal + ) mainContentView.splitDisconnectButton.secondaryButton.accessibilityLabel = NSLocalizedString( "RECONNECT_BUTTON_ACCESSIBILITY_LABEL", tableName: "Main", @@ -187,8 +208,21 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen private func updateTunnelConnectionInfo() { switch tunnelState { - case .connected(let connectionInfo), - .reconnecting(let connectionInfo): + case .connecting(let connectionInfo): + setConnectionInfo(connectionInfo) + + case .connected(let connectionInfo), .reconnecting(let connectionInfo): + setConnectionInfo(connectionInfo) + + case .disconnected, .disconnecting, .pendingReconnect: + setConnectionInfo(nil) + } + + mainContentView.locationContainerView.accessibilityLabel = tunnelState.localizedAccessibilityLabel + } + + private func setConnectionInfo(_ connectionInfo: TunnelConnectionInfo?) { + if let connectionInfo = connectionInfo { mainContentView.cityLabel.attributedText = attributedStringForLocation(string: connectionInfo.location.city) mainContentView.countryLabel.attributedText = attributedStringForLocation(string: connectionInfo.location.country) @@ -198,15 +232,12 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen ) mainContentView.connectionPanel.isHidden = false mainContentView.connectionPanel.connectedRelayName = connectionInfo.hostname - - case .connecting, .disconnected, .disconnecting: + } else { mainContentView.countryLabel.attributedText = attributedStringForLocation(string: " ") mainContentView.cityLabel.attributedText = attributedStringForLocation(string: " ") mainContentView.connectionPanel.dataSource = nil mainContentView.connectionPanel.isHidden = true } - - mainContentView.locationContainerView.accessibilityLabel = tunnelState.localizedAccessibilityLabel } private func locationMarkerOffset() -> CGPoint { @@ -243,38 +274,48 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen private func updateLocation(animated: Bool) { switch tunnelState { - case .connected(let connectionInfo), - .reconnecting(let connectionInfo): - let coordinate = connectionInfo.location.geoCoordinate - if let lastLocation = self.lastLocation, coordinate.approximatelyEqualTo(lastLocation) { - return + case .connecting(let connectionInfo): + if let connectionInfo = connectionInfo { + setLocation(coordinate: connectionInfo.location.geoCoordinate, animated: animated) + } else { + unsetLocation(animated: animated) } - let markerOffset = locationMarkerOffset() - let region = computeCoordinateRegion(centerCoordinate: coordinate, centerOffsetInPoints: markerOffset) + case .connected(let connectionInfo), .reconnecting(let connectionInfo): + setLocation(coordinate: connectionInfo.location.geoCoordinate, animated: animated) - locationMarker.coordinate = coordinate - mainContentView.mapView.addAnnotation(locationMarker) - mainContentView.mapView.setRegion(region, animated: animated) + case .disconnected, .disconnecting, .pendingReconnect: + unsetLocation(animated: animated) + } + } - self.lastLocation = coordinate + private func setLocation(coordinate: CLLocationCoordinate2D, animated: Bool) { + if let lastLocation = self.lastLocation, coordinate.approximatelyEqualTo(lastLocation) { + return + } - case .disconnected, .disconnecting: - let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0) - if let lastLocation = self.lastLocation, coordinate.approximatelyEqualTo(lastLocation) { - return - } + let markerOffset = locationMarkerOffset() + let region = computeCoordinateRegion(centerCoordinate: coordinate, centerOffsetInPoints: markerOffset) - let span = MKCoordinateSpan(latitudeDelta: 90, longitudeDelta: 90) - let region = MKCoordinateRegion(center: coordinate, span: span) - mainContentView.mapView.removeAnnotation(locationMarker) - mainContentView.mapView.setRegion(region, animated: animated) + locationMarker.coordinate = coordinate + mainContentView.mapView.addAnnotation(locationMarker) + mainContentView.mapView.setRegion(region, animated: animated) - self.lastLocation = coordinate + self.lastLocation = coordinate + } - case .connecting: - break + private func unsetLocation(animated: Bool) { + let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0) + if let lastLocation = self.lastLocation, coordinate.approximatelyEqualTo(lastLocation) { + return } + + let span = MKCoordinateSpan(latitudeDelta: 90, longitudeDelta: 90) + let region = MKCoordinateRegion(center: coordinate, span: span) + mainContentView.mapView.removeAnnotation(locationMarker) + mainContentView.mapView.setRegion(region, animated: animated) + + self.lastLocation = coordinate } private func addNotificationController() { @@ -296,15 +337,15 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen // MARK: - Actions @objc func handleConnect(_ sender: Any) { - delegate?.connectViewControllerShouldConnectTunnel(self) + TunnelManager.shared.startTunnel() } @objc func handleDisconnect(_ sender: Any) { - delegate?.connectViewControllerShouldDisconnectTunnel(self) + TunnelManager.shared.stopTunnel() } @objc func handleReconnect(_ sender: Any) { - delegate?.connectViewControllerShouldReconnectTunnel(self) + TunnelManager.shared.reconnectTunnel() } @objc func handleSelectLocation(_ sender: Any) { @@ -405,7 +446,7 @@ private extension TunnelState { case .connected: return .successColor - case .disconnecting, .disconnected: + case .disconnecting, .disconnected, .pendingReconnect: return .dangerColor } } @@ -428,7 +469,22 @@ private extension TunnelState { comment: "" ) - case .disconnecting, .disconnected: + case .disconnecting(.nothing): + return NSLocalizedString( + "TUNNEL_STATE_DISCONNECTING", + tableName: "Main", + value: "Disconnecting", + comment: "" + ) + case .disconnecting(.reconnect), .pendingReconnect: + return NSLocalizedString( + "TUNNEL_STATE_PENDING_RECONNECT", + tableName: "Main", + value: "Reconnecting", + comment: "" + ) + + case .disconnected: return NSLocalizedString( "TUNNEL_STATE_DISCONNECTED", tableName: "Main", @@ -440,46 +496,26 @@ private extension TunnelState { var localizedTitleForSelectLocationButton: String? { switch self { - case .disconnected, .disconnecting: - return NSLocalizedString( - "SELECT_LOCATION_BUTTON_TITLE", - tableName: "Main", - value: "Select location", - comment: "" - ) - case .connecting, .connected, .reconnecting: + case .disconnecting(.reconnect), .pendingReconnect: return NSLocalizedString( "SWITCH_LOCATION_BUTTON_TITLE", tableName: "Main", - value: "Switch location", + value: "Select location", comment: "" ) - } - } - var localizedTitleForConnectButton: String { - return NSLocalizedString( - "CONNECT_BUTTON_TITLE", - tableName: "Main", - value: "Secure connection", - comment: "" - ) - } - - var localizedTitleForDisconnectButton: String { - switch self { - case .connecting: + case .disconnected, .disconnecting(.nothing): return NSLocalizedString( - "CANCEL_BUTTON_TITLE", + "SELECT_LOCATION_BUTTON_TITLE", tableName: "Main", - value: "Cancel", + value: "Select location", comment: "" ) - case .connected, .reconnecting, .disconnecting, .disconnected: + case .connecting, .connected, .reconnecting: return NSLocalizedString( - "DISCONNECT_BUTTON_TITLE", + "SWITCH_LOCATION_BUTTON_TITLE", tableName: "Main", - value: "Disconnect", + value: "Switch location", comment: "" ) } @@ -527,13 +563,21 @@ private extension TunnelState { tunnelInfo.location.country ) - case .disconnecting: + case .disconnecting(.nothing): return NSLocalizedString( "TUNNEL_STATE_DISCONNECTING_ACCESSIBILITY_LABEL", tableName: "Main", value: "Disconnecting", comment: "" ) + + case .disconnecting(.reconnect), .pendingReconnect: + return NSLocalizedString( + "TUNNEL_STATE_PENDING_RECONNECT_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Reconnecting", + comment: "" + ) } } @@ -541,18 +585,24 @@ private extension TunnelState { switch (traitCollection.userInterfaceIdiom, traitCollection.horizontalSizeClass) { case (.phone, _), (.pad, .compact): switch self { - case .disconnected, .disconnecting: + case .disconnected, .disconnecting(.nothing): return [.selectLocation, .connect] - case .connecting, .connected, .reconnecting: + case .connecting, .pendingReconnect, .disconnecting(.reconnect): + return [.selectLocation, .cancel] + + case .connected, .reconnecting: return [.selectLocation, .disconnect] } case (.pad, .regular): switch self { - case .disconnected, .disconnecting: + case .disconnected, .disconnecting(.nothing): return [.connect] + case .disconnecting(.reconnect), .pendingReconnect: + return [.cancel] + case .connecting, .connected, .reconnecting: return [.disconnect] } diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift index 56f9103cb8..86ebb94b1f 100644 --- a/ios/MullvadVPN/DisplayChainedError.swift +++ b/ios/MullvadVPN/DisplayChainedError.swift @@ -236,19 +236,6 @@ extension TunnelManager.Error: DisplayChainedError { // This error is never displayed anywhere return nil - case .verifyWireguardKey(let restError): - let reason = restError.errorChainDescription ?? "" - - return String( - format: NSLocalizedString( - "VERIFY_WIREGUARD_KEY_ERROR", - tableName: "TunnelManager", - value: "Failed to verify the WireGuard key on server: %@", - comment: "" - ), - reason - ) - case .missingAccount: return NSLocalizedString( "MISSING_ACCOUNT_INTERNAL_ERROR", @@ -256,6 +243,24 @@ extension TunnelManager.Error: DisplayChainedError { value: "Internal error: missing account", comment: "" ) + case .readRelays: + return NSLocalizedString( + "READ_RELAYS_ERROR", + tableName: "TunnelManager", + value: "Failed to read relays.", + comment: "" + ) + case .cannotSatisfyRelayConstraints: + return NSLocalizedString( + "CANNOT_SATISFY_RELAY_CONSTRAINTS_ERROR", + tableName: "TunnelManager", + value: "Failed to satisfy relay constraints.", + comment: "" + ) + + case .backgroundTaskScheduler: + // This error is never displayed anywhere + return nil } } } diff --git a/ios/MullvadVPN/IPAddressRange+Codable.swift b/ios/MullvadVPN/IPAddressRange+Codable.swift index f571227849..79f9e00a65 100644 --- a/ios/MullvadVPN/IPAddressRange+Codable.swift +++ b/ios/MullvadVPN/IPAddressRange+Codable.swift @@ -7,7 +7,7 @@ // import Foundation -import WireGuardKit +import struct WireGuardKit.IPAddressRange extension IPAddressRange: Codable { public func encode(to encoder: Encoder) throws { diff --git a/ios/MullvadVPN/Location.swift b/ios/MullvadVPN/Location.swift index c29c0a707e..1d10f4c86e 100644 --- a/ios/MullvadVPN/Location.swift +++ b/ios/MullvadVPN/Location.swift @@ -7,7 +7,7 @@ // import Foundation -import CoreLocation +import struct CoreLocation.CLLocationCoordinate2D struct Location: Codable, Equatable { var country: String diff --git a/ios/MullvadVPN/NEVPNStatus+Debug.swift b/ios/MullvadVPN/NEVPNStatus+Debug.swift index 387a9f73a6..ba612d8b1a 100644 --- a/ios/MullvadVPN/NEVPNStatus+Debug.swift +++ b/ios/MullvadVPN/NEVPNStatus+Debug.swift @@ -9,25 +9,23 @@ import Foundation import NetworkExtension -extension NEVPNStatus: CustomDebugStringConvertible { - public var debugDescription: String { - var output = "NEVPNStatus." +extension NEVPNStatus: CustomStringConvertible { + public var description: String { switch self { case .connected: - output += "connected" + return "connected" case .connecting: - output += "connecting" + return "connecting" case .disconnected: - output += "disconnected" + return "disconnected" case .disconnecting: - output += "disconnecting" + return "disconnecting" case .invalid: - output += "invalid" + return "invalid" case .reasserting: - output += "reasserting" + return "reasserting" @unknown default: - output += "\(self.rawValue)" + return "unknown value (\(self.rawValue))" } - return output } } diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/PreferencesViewController.swift index a1fb0cd2d4..6f32ca970d 100644 --- a/ios/MullvadVPN/PreferencesViewController.swift +++ b/ios/MullvadVPN/PreferencesViewController.swift @@ -48,23 +48,25 @@ class PreferencesViewController: UITableViewController, TunnelObserver { navigationItem.largeTitleDisplayMode = .always TunnelManager.shared.addObserver(self) - self.dnsSettings = TunnelManager.shared.tunnelSettings?.interface.dnsSettings + self.dnsSettings = TunnelManager.shared.tunnelInfo?.tunnelSettings.interface.dnsSettings setupDataSource() } // MARK: - TunnelObserver - func tunnelStateDidChange(tunnelState: TunnelState) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { // no-op } - func tunnelSettingsDidChange(tunnelSettings: TunnelSettings?) { - DispatchQueue.main.async { - if tunnelSettings?.interface.dnsSettings != self.dnsSettings { - self.dnsSettings = tunnelSettings?.interface.dnsSettings - self.tableView.reloadData() - } + func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + // no-op + } + + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) { + if tunnelInfo?.tunnelSettings.interface.dnsSettings != self.dnsSettings { + self.dnsSettings = tunnelInfo?.tunnelSettings.interface.dnsSettings + self.tableView.reloadData() } } @@ -103,11 +105,11 @@ class PreferencesViewController: UITableViewController, TunnelObserver { private func saveDNSSettings() { guard let dnsSettings = dnsSettings else { return } - TunnelManager.shared.setDNSSettings(dnsSettings) { [weak self] (result) in - if case .failure(let error) = result { + TunnelManager.shared.setDNSSettings(dnsSettings) + .onFailure { [weak self] error in self?.logger.error(chainedError: error, message: "Failed to save DNS settings") } - } + .observe { _ in } } } diff --git a/ios/MullvadVPN/PrivateKeyWithMetadata.swift b/ios/MullvadVPN/PrivateKeyWithMetadata.swift index b3e4990aee..fb39027817 100644 --- a/ios/MullvadVPN/PrivateKeyWithMetadata.swift +++ b/ios/MullvadVPN/PrivateKeyWithMetadata.swift @@ -7,7 +7,8 @@ // import Foundation -import WireGuardKit +import class WireGuardKit.PrivateKey +import class WireGuardKit.PublicKey /// A struct holding a private WireGuard key with associated metadata struct PrivateKeyWithMetadata: Equatable { @@ -23,6 +24,11 @@ struct PrivateKeyWithMetadata: Equatable { return PublicKeyWithMetadata(publicKey: privateKey.publicKey, createdAt: creationDate) } + /// Public key + var publicKey: PublicKey { + return privateKey.publicKey + } + /// Initialize the new private key init() { privateKey = PrivateKey() diff --git a/ios/MullvadVPN/Promise/Promise+Delay.swift b/ios/MullvadVPN/Promise/Promise+Delay.swift new file mode 100644 index 0000000000..54f497d04b --- /dev/null +++ b/ios/MullvadVPN/Promise/Promise+Delay.swift @@ -0,0 +1,37 @@ +// +// Promise+Delay.swift +// Promise+Delay +// +// Created by pronebird on 07/09/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension Promise { + /// Delay observing the upstream by the given interval. + func delay(by timeInterval: DispatchTimeInterval, timerType: TimerType, queue: DispatchQueue? = nil) -> Promise<Value> { + return Promise<Value> { resolver in + let timer = DispatchSource.makeTimerSource(flags: [], queue: queue) + timer.setEventHandler { + self.observe { completion in + resolver.resolve(completion: completion, queue: nil) + } + } + + resolver.setCancelHandler { + timer.cancel() + } + + switch timerType { + case .deadline: + timer.schedule(deadline: .now() + timeInterval) + + case .walltime: + timer.schedule(wallDeadline: .now() + timeInterval) + } + + timer.activate() + } + } +} diff --git a/ios/MullvadVPN/REST/RESTClient.swift b/ios/MullvadVPN/REST/RESTClient.swift index c0b0caa9dd..2c76241ae9 100644 --- a/ios/MullvadVPN/REST/RESTClient.swift +++ b/ios/MullvadVPN/REST/RESTClient.swift @@ -8,7 +8,8 @@ import Foundation import Network -import WireGuardKit +import class WireGuardKit.PublicKey +import struct WireGuardKit.IPAddressRange extension REST { diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift index 2aa95403f7..cc1cbeef52 100644 --- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift @@ -9,8 +9,7 @@ #if targetEnvironment(simulator) import Foundation -import Network -import NetworkExtension +import enum NetworkExtension.NEProviderStopReason import Logging class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { diff --git a/ios/MullvadVPN/TunnelManager.swift b/ios/MullvadVPN/TunnelManager.swift deleted file mode 100644 index 23ccd3d324..0000000000 --- a/ios/MullvadVPN/TunnelManager.swift +++ /dev/null @@ -1,1196 +0,0 @@ -// -// TunnelManager.swift -// MullvadVPN -// -// Created by pronebird on 25/09/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import NetworkExtension -import Logging -import WireGuardKit - -enum MapConnectionStatusError: ChainedError { - /// A failure to perform the IPC request because the tunnel IPC is already deallocated - case missingIpc - - /// A failure to send a subsequent IPC request to collect more information, such as tunnel - /// connection info. - case ipcRequest(PacketTunnelIpc.Error) - - /// A failure to map the status because the unknown variant of `NEVPNStatus` was given. - case unknownStatus(NEVPNStatus) - - /// A failure to map the status because the `NEVPNStatus.invalid` variant was given - /// This happens when attempting to start a tunnel with configuration that does not exist - /// anymore in system preferences. - case invalidConfiguration - - var errorDescription: String? { - switch self { - case .missingIpc: - return "Missing IPC" - - case .ipcRequest: - return "IPC request error" - - case .unknownStatus(let status): - return "Unknown NEVPNStatus: \(status)" - - case .invalidConfiguration: - return "Invalid VPN configuration" - } - } -} - -/// A enum that describes the tunnel state -enum TunnelState: Equatable { - /// Connecting the tunnel - case connecting - - /// Connected the tunnel - case connected(TunnelConnectionInfo) - - /// Disconnecting the tunnel - case disconnecting - - /// Disconnected the tunnel - case disconnected - - /// Reconnecting the tunnel. Normally this state appears in response to changing the - /// relay constraints and asking the running tunnel to reload the configuration. - case reconnecting(TunnelConnectionInfo) -} - -extension TunnelState: CustomStringConvertible, CustomDebugStringConvertible { - var description: String { - switch self { - case .connecting: - return "connecting" - case .connected: - return "connected" - case .disconnecting: - return "disconnecting" - case .disconnected: - return "disconnected" - case .reconnecting: - return "reconnecting" - } - } - - var debugDescription: String { - var output = "TunnelState." - - switch self { - case .connecting: - output.append("connecting") - - case .connected(let connectionInfo): - output.append("connected(") - output.append(String(reflecting: connectionInfo)) - output.append(")") - - case .disconnecting: - output.append("disconnecting") - - case .disconnected: - output.append("disconnected") - - case .reconnecting(let connectionInfo): - output.append("reconnecting(") - output.append(String(reflecting: connectionInfo)) - output.append(")") - } - - return output - } -} - -protocol TunnelObserver: AnyObject { - func tunnelStateDidChange(tunnelState: TunnelState) - func tunnelSettingsDidChange(tunnelSettings: TunnelSettings?) -} - -private class AnyTunnelObserver: WeakObserverBox, TunnelObserver { - - typealias Wrapped = TunnelObserver - - private(set) weak var inner: TunnelObserver? - - init<T: TunnelObserver>(_ inner: T) { - self.inner = inner - } - - func tunnelStateDidChange(tunnelState: TunnelState) { - self.inner?.tunnelStateDidChange(tunnelState: tunnelState) - } - - func tunnelSettingsDidChange(tunnelSettings: TunnelSettings?) { - self.inner?.tunnelSettingsDidChange(tunnelSettings: tunnelSettings) - } - - static func == (lhs: AnyTunnelObserver, rhs: AnyTunnelObserver) -> Bool { - return lhs.inner === rhs.inner - } -} - -/// A class that provides a convenient interface for VPN tunnels configuration, manipulation and -/// monitoring. -class TunnelManager { - - /// An error emitted by all public methods of TunnelManager - enum Error: ChainedError { - /// Account token is not set - case missingAccount - - /// A failure to start the VPN tunnel via system call - case startVPNTunnel(Swift.Error) - - /// A failure to load the system VPN configurations created by the app - case loadAllVPNConfigurations(Swift.Error) - - /// A failure to save the system VPN configuration - case saveVPNConfiguration(Swift.Error) - - /// A failure to reload the system VPN configuration - case reloadVPNConfiguration(Swift.Error) - - /// A failure to remove the system VPN configuration - case removeVPNConfiguration(Swift.Error) - - /// A failure to perform a recovery (by removing the VPN configuration) when a corrupt - /// VPN configuration is detected. - case removeInconsistentVPNConfiguration(Swift.Error) - - /// A failure to read tunnel settings - case readTunnelSettings(TunnelSettingsManager.Error) - - /// A failure to add the tunnel settings - case addTunnelSettings(TunnelSettingsManager.Error) - - /// A failure to update the tunnel settings - case updateTunnelSettings(TunnelSettingsManager.Error) - - /// A failure to remove the tunnel settings from Keychain - case removeTunnelSettings(TunnelSettingsManager.Error) - - /// A failure to migrate tunnel settings - case migrateTunnelSettings(TunnelSettingsManager.Error) - - /// Unable to obtain the persistent keychain reference for the tunnel settings - case obtainPersistentKeychainReference(TunnelSettingsManager.Error) - - /// A failure to push the public WireGuard key - case pushWireguardKey(RestError) - - /// A failure to replace the public WireGuard key - case replaceWireguardKey(RestError) - - /// A failure to remove the public WireGuard key - case removeWireguardKey(RestError) - - /// A failure to verify the public WireGuard key - case verifyWireguardKey(RestError) - - var errorDescription: String? { - switch self { - case .missingAccount: - return "Missing account token" - case .startVPNTunnel: - return "Failed to start the VPN tunnel" - case .loadAllVPNConfigurations: - return "Failed to load the system VPN configurations" - case .saveVPNConfiguration: - return "Failed to save the system VPN configuration" - case .reloadVPNConfiguration: - return "Failed to reload the system VPN configuration" - case .removeVPNConfiguration: - return "Failed to remove the system VPN configuration" - case .removeInconsistentVPNConfiguration: - return "Failed to remove the inconsistent VPN tunnel" - case .readTunnelSettings: - return "Failed to read the tunnel settings" - case .addTunnelSettings: - return "Failed to add the tunnel settings" - case .updateTunnelSettings: - return "Failed to update the tunnel settings" - case .removeTunnelSettings: - return "Failed to remove the tunnel settings" - case .migrateTunnelSettings: - return "Failed to migrate the tunnel settings" - case .obtainPersistentKeychainReference: - return "Failed to obtain the persistent keychain reference" - case .pushWireguardKey: - return "Failed to push the WireGuard key to server" - case .replaceWireguardKey: - return "Failed to replace the WireGuard key on server" - case .removeWireguardKey: - return "Failed to remove the WireGuard key from server" - case .verifyWireguardKey: - return "Failed to verify the WireGuard key on server" - } - } - } - - // Switch to stabs on simulator - #if targetEnvironment(simulator) - typealias TunnelProviderManagerType = SimulatorTunnelProviderManager - #else - typealias TunnelProviderManagerType = NETunnelProviderManager - #endif - - static let shared = TunnelManager() - - // MARK: - Internal variables - - private let logger = Logger(label: "TunnelManager") - private let dispatchQueue = DispatchQueue(label: "net.mullvad.MullvadVPN.TunnelManager") - - private let rest = MullvadRest() - private var tunnelProvider: TunnelProviderManagerType? - private var tunnelIpc: PacketTunnelIpc? - - private let stateLock = NSLock() - private let observerList = ObserverList<AnyTunnelObserver>() - - /// A VPN connection status observer - private var connectionStatusObserver: NSObjectProtocol? - - /// An account token associated with the active tunnel - private var accountToken: String? - - private var _tunnelState = TunnelState.disconnected - private var _tunnelSettings: TunnelSettings? - - private init() {} - - // MARK: - Public - - private(set) var tunnelState: TunnelState { - set { - stateLock.withCriticalBlock { - guard _tunnelState != newValue else { return } - - logger.info("Set tunnel state: \(newValue)") - - _tunnelState = newValue - - observerList.forEach { (observer) in - observer.tunnelStateDidChange(tunnelState: newValue) - } - } - } - get { - stateLock.withCriticalBlock { - return _tunnelState - } - } - } - - /// The last known public key - private(set) var tunnelSettings: TunnelSettings? { - set { - stateLock.withCriticalBlock { - guard _tunnelSettings != newValue else { return } - - _tunnelSettings = newValue - - observerList.forEach { (observer) in - observer.tunnelSettingsDidChange(tunnelSettings: newValue) - } - } - } - get { - stateLock.withCriticalBlock { - return _tunnelSettings - } - } - } - - /// Initialize the TunnelManager with the tunnel from the system. - /// - /// The given account token is used to ensure that the system tunnel was configured for the same - /// account. The system tunnel is removed in case of inconsistency. - func loadTunnel(accountToken: String?, completionHandler: @escaping (Result<(), TunnelManager.Error>) -> Void) { - let operation = ResultOperation<(), TunnelManager.Error> { (finish) in - TunnelProviderManagerType.loadAllFromPreferences { (tunnels, error) in - self.dispatchQueue.async { - if let error = error { - finish(.failure(.loadAllVPNConfigurations(error))) - } else { - self.initializeManager(accountToken: accountToken, tunnels: tunnels, completionHandler: finish) - } - } - } - } - - operation.addDidFinishBlockObserver { (operation, result) in - completionHandler(result) - } - - exclusityController.addOperation(operation, categories: [.tunnelControl]) - } - - /// Refresh tunnel state. - /// Use this method to update the tunnel state when app transitions from suspended to active - /// state. - func refreshTunnelState(completionHandler: (() -> Void)?) { - let operation = BlockOperation { - // Reload the last known public key - if let accountToken = self.accountToken { - switch Self.loadTunnelSettings(accountToken: accountToken) { - case .success(let keychainEntry): - self.tunnelSettings = keychainEntry.tunnelSettings - case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to reload tunnel settings when refreshing tunnel state.") - } - } - - if let status = self.tunnelProvider?.connection.status { - self.updateTunnelState(connectionStatus: status) - } - - completionHandler?() - } - - exclusityController.addOperation(operation, categories: [.tunnelControl]) - } - - func startTunnel(completionHandler: @escaping (Result<(), Error>) -> Void) { - let operation = ResultOperation<(), Error> { (finish) in - guard let accountToken = self.accountToken else { - finish(.failure(.missingAccount)) - return - } - - self.makeTunnelProvider(accountToken: accountToken) { (result) in - let result = result.flatMap { (tunnelProvider) -> Result<(), Error> in - self.setTunnelProvider(tunnelProvider: tunnelProvider) - - return Result { try tunnelProvider.connection.startVPNTunnel() } - .mapError { Error.startVPNTunnel($0) } - } - finish(result) - } - } - - operation.addDidFinishBlockObserver { (operation, result) in - completionHandler(result) - } - - exclusityController.addOperation(operation, categories: [.tunnelControl]) - } - - func stopTunnel(completionHandler: @escaping (Result<(), Error>) -> Void) { - let operation = ResultOperation<(), Error> { (finish) in - guard let tunnelProvider = self.tunnelProvider else { - finish(.success(())) - return - } - - // Disable on-demand when stopping the tunnel to prevent it from coming back up - tunnelProvider.isOnDemandEnabled = false - - tunnelProvider.saveToPreferences { (error) in - if let error = error { - finish(.failure(.saveVPNConfiguration(error))) - } else { - tunnelProvider.connection.stopVPNTunnel() - finish(.success(())) - } - } - } - - operation.addDidFinishBlockObserver { (operation, result) in - completionHandler(result) - } - - exclusityController.addOperation(operation, categories: [.tunnelControl]) - } - - func reconnectTunnel(completionHandler: (() -> Void)?) { - let operation = AsyncBlockOperation { (finish) in - guard let tunnelIpc = self.tunnelIpc else { - finish() - return - } - - tunnelIpc.reloadTunnelSettings { (result) in - if case .failure(let error) = result { - self.logger.error(chainedError: error, message: "Failed to reconnect the tunnel") - } - finish() - } - } - - operation.addDidFinishBlockObserver { (operation) in - completionHandler?() - } - - exclusityController.addOperation(operation, categories: [.tunnelControl]) - } - - func setAccount(accountToken: String, completionHandler: @escaping (Result<(), TunnelManager.Error>) -> Void) { - let operation = ResultOperation<(), TunnelManager.Error> { (finish) in - let result = Self.makeTunnelSettings(accountToken: accountToken) - - guard case .success(let tunnelSettings) = result else { - finish(result.map { _ in () }) - return - } - - let interfaceSettings = tunnelSettings.interface - let publicKeyWithMetadata = interfaceSettings.privateKey.publicKeyWithMetadata - - guard interfaceSettings.addresses.isEmpty else { - self.tunnelSettings = tunnelSettings - self.accountToken = accountToken - - finish(.success(())) - return - } - - // Push wireguard key if addresses were not received yet - self.pushWireguardKeyAndUpdateSettings(accountToken: accountToken, publicKey: publicKeyWithMetadata.publicKey) { (result) in - if case .success(let newTunnelSettings) = result { - self.tunnelSettings = newTunnelSettings - self.accountToken = accountToken - } - finish(result.map { _ in () }) - } - } - operation.addDidFinishBlockObserver { (operation, result) in - completionHandler(result) - } - - exclusityController.addOperation(operation, categories: [.tunnelControl]) - } - - /// Remove the account token and remove the active tunnel - func unsetAccount(completionHandler: @escaping (Result<(), TunnelManager.Error>) -> Void) { - let operation = ResultOperation<(), TunnelManager.Error> { (finish) in - guard let accountToken = self.accountToken else { - finish(.failure(.missingAccount)) - return - } - - let completeOperation = { - self.accountToken = nil - self.tunnelSettings = nil - - finish(.success(())) - } - - let removeTunnel = { - // Unregister from receiving the tunnel state changes - self.unregisterConnectionObserver() - self.tunnelState = .disconnected - self.tunnelIpc = nil - - // Remove settings from Keychain - switch TunnelSettingsManager.remove(searchTerm: .accountToken(accountToken)) { - case .success: - break - case .failure(let error): - // Ignore Keychain errors because that normally means that the Keychain - // configuration was already removed and we shouldn't be blocking the - // user from logging out - self.logger.error(chainedError: error, message: "Unset account error") - } - - guard let tunnelProvider = self.tunnelProvider else { - completeOperation() - return - } - - self.tunnelProvider = nil - - // Remove VPN configuration - tunnelProvider.removeFromPreferences(completionHandler: { (error) in - self.dispatchQueue.async { - if let error = error { - // Ignore error if the tunnel was already removed by user - if let systemError = error as? NEVPNError, systemError.code == .configurationInvalid { - completeOperation() - } else { - finish(.failure(.removeVPNConfiguration(error))) - } - } else { - completeOperation() - } - } - }) - } - - switch Self.loadTunnelSettings(accountToken: accountToken) { - case .success(let keychainEntry): - let publicKey = keychainEntry.tunnelSettings - .interface - .privateKey - .publicKeyWithMetadata - .publicKey - - self.removeWireguardKeyFromServer(accountToken: accountToken, publicKey: publicKey) { (result) in - switch result { - case .success(let isRemoved): - self.logger.warning("Removed the WireGuard key from server: \(isRemoved)") - - case .failure(let error): - self.logger.error(chainedError: error, message: "Unset account error") - } - - removeTunnel() - } - - case .failure(let error): - // Ignore Keychain errors because that normally means that the Keychain - // configuration was already removed and we shouldn't be blocking the - // user from logging out - self.logger.error(chainedError: error, message: "Unset account error") - - removeTunnel() - } - - } - - operation.addDidFinishBlockObserver { (operation, result) in - completionHandler(result) - } - - exclusityController.addOperation(operation, categories: [.tunnelControl]) - } - - func verifyPublicKey(completionHandler: @escaping (Result<Bool, Error>) -> Void) { - let makePayloadOperation = ResultOperation<PublicKeyPayload<TokenPayload<EmptyPayload>>, Error> { - () -> Result<PublicKeyPayload<TokenPayload<EmptyPayload>>, Error> in - guard let accountToken = self.accountToken else { - return .failure(.missingAccount) - } - - return Self.loadTunnelSettings(accountToken: accountToken) - .map { (keychainEntry) -> PublicKeyPayload<TokenPayload<EmptyPayload>> in - let publicKey = keychainEntry.tunnelSettings.interface - .privateKey - .publicKeyWithMetadata.publicKey.rawValue - - return PublicKeyPayload( - pubKey: publicKey, - payload: TokenPayload(token: keychainEntry.accountToken, payload: EmptyPayload()) - ) - } - } - - let getPubkeyOperation = self.rest.getWireguardKey() - .operation(payload: nil) - .injectResult(from: makePayloadOperation) - - getPubkeyOperation.addDidFinishBlockObserver { (operation, result) in - let result = result.map { (_) -> Bool in - return true - }.mapError { (restError) -> Error in - return .verifyWireguardKey(restError) - } - - completionHandler(result) - } - - operationQueue.addOperations([makePayloadOperation, getPubkeyOperation], waitUntilFinished: false) - } - - func regeneratePrivateKey(completionHandler: @escaping (Result<(), Error>) -> Void) { - let operation = ResultOperation<(), Error> { (finish) in - guard let accountToken = self.accountToken else { - finish(.failure(.missingAccount)) - return - } - - let result = Self.loadTunnelSettings(accountToken: accountToken) - guard case .success(let keychainEntry) = result else { - finish(result.map { _ in () }) - return - } - - let newPrivateKey = PrivateKeyWithMetadata() - let oldPublicKeyMetadata = keychainEntry.tunnelSettings.interface - .privateKey - .publicKeyWithMetadata - - self.replaceWireguardKeyAndUpdateSettings(accountToken: accountToken, oldPublicKey: oldPublicKeyMetadata, newPrivateKey: newPrivateKey) { (result) in - guard case .success(let newTunnelSettings) = result else { - finish(result.map { _ in () }) - return - } - - self.tunnelSettings = newTunnelSettings - - guard let tunnelIpc = self.tunnelIpc else { - finish(.success(())) - return - } - - tunnelIpc.reloadTunnelSettings { (ipcResult) in - if case .failure(let error) = ipcResult { - // Ignore Packet Tunnel IPC errors but log them - self.logger.error(chainedError: error, message: "Failed to IPC the tunnel to reload configuration") - } - - finish(.success(())) - } - } - } - - operation.addDidFinishBlockObserver { (operation, result) in - completionHandler(result) - } - - exclusityController.addOperation(operation, categories: [.tunnelControl]) - } - - func setRelayConstraints(_ constraints: RelayConstraints, completionHandler: @escaping (Result<(), TunnelManager.Error>) -> Void) { - self.addOperationToModifyTunnelSettingsAndNotifyPacketTunnel(usingBlock: { (tunnelSettings) in - tunnelSettings.relayConstraints = constraints - }, completionHandler: completionHandler) - } - - func setDNSSettings(_ dnsSettings: DNSSettings, completionHandler: @escaping (Result<(), TunnelManager.Error>) -> Void) { - self.addOperationToModifyTunnelSettingsAndNotifyPacketTunnel(usingBlock: { (tunnelSettings) in - tunnelSettings.interface.dnsSettings = dnsSettings - }, completionHandler: completionHandler) - } - - // MARK: - Tunnel observeration - - /// Add tunnel observer. - /// In order to cancel the observation, either call `removeTunnelObserver(_:)` or simply release - /// the observer. - func addObserver<T: TunnelObserver>(_ observer: T) { - observerList.append(AnyTunnelObserver(observer)) - } - - /// Remove tunnel observer. - func removeObserver<T: TunnelObserver>(_ observer: T) { - observerList.remove(AnyTunnelObserver(observer)) - } - - // MARK: - Operation management - - enum OperationCategory { - case tunnelControl - case stateUpdate - } - - private lazy var operationQueue: OperationQueue = { - let queue = OperationQueue() - queue.underlyingQueue = self.dispatchQueue - return queue - }() - private lazy var exclusityController: ExclusivityController<OperationCategory> = { - return ExclusivityController(operationQueue: self.operationQueue) - }() - - // MARK: - Private methods - - private func initializeManager(accountToken: String?, tunnels: [TunnelProviderManagerType]?, completionHandler: @escaping (Result<(), TunnelManager.Error>) -> Void) { - // Migrate the tunnel settings if needed - let migrationResult = accountToken.flatMap { self.migrateTunnelSettings(accountToken: $0) } - switch migrationResult { - case .success, .none: - break - case .failure(let migrationError): - completionHandler(.failure(migrationError)) - return - } - - switch (tunnels?.first, accountToken) { - // Case 1: tunnel exists and account token is set. - // Verify that tunnel can access the configuration via the persistent keychain reference - // stored in `passwordReference` field of VPN configuration. - case (.some(let tunnelProvider), .some(let accountToken)): - let verificationResult = self.verifyTunnel(tunnelProvider: tunnelProvider, expectedAccountToken: accountToken) - let tunnelSettingsResult = Self.loadTunnelSettings(accountToken: accountToken) - - switch (verificationResult, tunnelSettingsResult) { - case (.success(true), .success(let keychainEntry)): - self.accountToken = accountToken - self.tunnelSettings = keychainEntry.tunnelSettings - self.setTunnelProvider(tunnelProvider: tunnelProvider) - - completionHandler(.success(())) - - // Remove the tunnel when failed to verify it but successfuly loaded the tunnel - // settings. - case (.failure(let verificationError), .success(let keychainEntry)): - self.logger.error(chainedError: verificationError, message: "Failed to verify the tunnel but successfully loaded the tunnel settings. Removing the tunnel.") - - // Identical code path as the case below. - fallthrough - - // Remove the tunnel with corrupt configuration. - // It will be re-created upon the first attempt to connect the tunnel. - case (.success(false), .success(let keychainEntry)): - tunnelProvider.removeFromPreferences { (error) in - self.dispatchQueue.async { - if let error = error { - completionHandler(.failure(.removeInconsistentVPNConfiguration(error))) - } else { - self.accountToken = accountToken - self.tunnelSettings = keychainEntry.tunnelSettings - - completionHandler(.success(())) - } - } - } - - // Remove the tunnel when failed to verify the tunnel and load tunnel settings. - case (.failure(let verificationError), .failure(_)): - self.logger.error(chainedError: verificationError, message: "Failed to verify the tunnel and load tunnel settings. Removing the tunnel.") - - tunnelProvider.removeFromPreferences { (error) in - if let error = error { - completionHandler(.failure(.removeInconsistentVPNConfiguration(error))) - } else { - completionHandler(.failure(verificationError)) - } - } - - // Remove the tunnel when the app is not able to read tunnel settings - case (.success(_), .failure(let settingsReadError)): - self.logger.error(chainedError: settingsReadError, message: "Failed to load tunnel settings. Removing the tunnel.") - - tunnelProvider.removeFromPreferences { (error) in - if let error = error { - completionHandler(.failure(.removeInconsistentVPNConfiguration(error))) - } else { - completionHandler(.failure(settingsReadError)) - } - } - } - - // Case 2: tunnel exists but account token is unset. - // Remove the orphaned tunnel. - case (.some(let tunnelProvider), .none): - tunnelProvider.removeFromPreferences { (error) in - self.dispatchQueue.async { - if let error = error { - completionHandler(.failure(.removeInconsistentVPNConfiguration(error))) - } else { - completionHandler(.success(())) - } - } - } - - // Case 3: tunnel does not exist but the account token is set. - // Verify that tunnel settings exists in keychain. - case (.none, .some(let accountToken)): - switch Self.loadTunnelSettings(accountToken: accountToken) { - case .success(let keychainEntry): - self.accountToken = accountToken - self.tunnelSettings = keychainEntry.tunnelSettings - - completionHandler(.success(())) - - case .failure(let error): - completionHandler(.failure(error)) - } - - // Case 4: no tunnels exist and account token is unset. - case (.none, .none): - completionHandler(.success(())) - } - } - - private func verifyTunnel(tunnelProvider: TunnelProviderManagerType, expectedAccountToken accountToken: String) -> Result<Bool, Error> { - // Check that the VPN configuration points to the same account token - guard let username = tunnelProvider.protocolConfiguration?.username, username == accountToken else { - logger.warning("The token assigned to the VPN configuration does not match the logged in account.") - return .success(false) - } - - // Check that the passwordReference, containing the keychain reference for tunnel - // configuration, is set. - guard let keychainReference = tunnelProvider.protocolConfiguration?.passwordReference else { - logger.warning("VPN configuration is missing the passwordReference.") - return .success(false) - } - - // Verify that the keychain reference points to the existing entry in Keychain. - // Bad reference is possible when migrating the user data from one device to the other. - return TunnelSettingsManager.exists(searchTerm: .persistentReference(keychainReference)) - .mapError { (error) -> Error in - logger.error(chainedError: error, message: "Failed to verify the persistent keychain reference for tunnel settings.") - - return Error.readTunnelSettings(error) - } - } - - /// Set the instance of the active tunnel and add the tunnel status observer - private func setTunnelProvider(tunnelProvider: TunnelProviderManagerType) { - guard self.tunnelProvider != tunnelProvider else { - return - } - - // Save the new active tunnel provider - self.tunnelProvider = tunnelProvider - - // Set up tunnel IPC - let connection = tunnelProvider.connection - let session = connection as! VPNTunnelProviderSessionProtocol - let tunnelIpc = PacketTunnelIpc(session: session) - self.tunnelIpc = tunnelIpc - - // Register for tunnel connection status changes - unregisterConnectionObserver() - connectionStatusObserver = NotificationCenter.default - .addObserver(forName: .NEVPNStatusDidChange, object: connection, queue: nil) { - [weak self] (notification) in - guard let self = self else { return } - - let connection = notification.object as? VPNConnectionProtocol - - if let status = connection?.status { - self.updateTunnelState(connectionStatus: status) - } - } - - // Update the existing state - updateTunnelState(connectionStatus: connection.status) - } - - private func unregisterConnectionObserver() { - if let connectionStatusObserver = connectionStatusObserver { - NotificationCenter.default.removeObserver(connectionStatusObserver) - self.connectionStatusObserver = nil - } - } - - private func pushWireguardKeyAndUpdateSettings( - accountToken: String, - publicKey: PublicKey, - completionHandler: @escaping (Result<TunnelSettings, Error>) -> Void) - { - let payload = TokenPayload(token: accountToken, payload: PushWireguardKeyRequest(pubkey: publicKey.rawValue)) - let operation = rest.pushWireguardKey().operation(payload: payload) - - operation.addDidFinishBlockObserver(queue: dispatchQueue) { (operation, result) in - let updateResult = result - .mapError({ (restError) -> Error in - return .pushWireguardKey(restError) - }) - .flatMap { (associatedAddresses) -> Result<TunnelSettings, Error> in - return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in - tunnelSettings.interface.addresses = [ - associatedAddresses.ipv4Address, - associatedAddresses.ipv6Address - ] - } - } - - completionHandler(updateResult) - } - - operationQueue.addOperation(operation) - } - - private func removeWireguardKeyFromServer(accountToken: String, publicKey: PublicKey, completionHandler: @escaping (Result<Bool, Error>) -> Void) { - let payload = PublicKeyPayload(pubKey: publicKey.rawValue, payload: TokenPayload(token: accountToken, payload: EmptyPayload())) - let operation = rest.deleteWireguardKey().operation(payload: payload) - - operation.addDidFinishBlockObserver(queue: dispatchQueue) { (operation, result) in - let result = result.map({ () -> Bool in - return true - }).flatMapError { (restError) -> Result<Bool, Error> in - if case .server(.pubKeyNotFound) = restError { - return .success(false) - } else { - return .failure(.removeWireguardKey(restError)) - } - } - - completionHandler(result) - } - - operationQueue.addOperation(operation) - } - - private func replaceWireguardKeyAndUpdateSettings( - accountToken: String, - oldPublicKey: PublicKeyWithMetadata, - newPrivateKey: PrivateKeyWithMetadata, - completionHandler: @escaping (Result<TunnelSettings, Error>) -> Void) - { - let payload = TokenPayload( - token: accountToken, - payload: ReplaceWireguardKeyRequest( - old: oldPublicKey.publicKey.rawValue, - new: newPrivateKey.publicKeyWithMetadata.publicKey.rawValue - ) - ) - - let operation = rest.replaceWireguardKey().operation(payload: payload) - - operation.addDidFinishBlockObserver(queue: dispatchQueue) { (operation, result) in - let updateResult = result - .mapError({ (restError) -> Error in - return .replaceWireguardKey(restError) - }) - .flatMap { (associatedAddresses) -> Result<TunnelSettings, Error> in - return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in - tunnelSettings.interface.privateKey = newPrivateKey - tunnelSettings.interface.addresses = [ - associatedAddresses.ipv4Address, - associatedAddresses.ipv6Address - ] - } - } - - completionHandler(updateResult) - } - - operationQueue.addOperation(operation) - } - - /// Modify tunnel settings in Keychain and tell Packet Tunnel to reload. - private func addOperationToModifyTunnelSettingsAndNotifyPacketTunnel(usingBlock block: @escaping (inout TunnelSettings) -> Void, completionHandler: @escaping (Result<(), TunnelManager.Error>) -> Void) { - let operation = ResultOperation<(), TunnelManager.Error> { (finish) in - guard let accountToken = self.accountToken else { - finish(.failure(.missingAccount)) - return - } - - let result = Self.updateTunnelSettings(accountToken: accountToken, block: block) - - guard case .success(let newTunnelSettings) = result else { - finish(result.map { _ in () }) - return - } - - self.tunnelSettings = newTunnelSettings - - guard let tunnelIpc = self.tunnelIpc else { - finish(.success(())) - return - } - - tunnelIpc.reloadTunnelSettings { (ipcResult) in - // Ignore Packet Tunnel IPC errors but log them - if case .failure(let error) = ipcResult { - self.logger.error(chainedError: error, message: "Failed to reload tunnel settings") - } - - finish(.success(())) - } - } - - operation.addDidFinishBlockObserver { (operation, result) in - completionHandler(result) - } - - exclusityController.addOperation(operation, categories: [.tunnelControl]) - } - - /// Initiates the `tunnelState` update - private func updateTunnelState(connectionStatus: NEVPNStatus) { - let operation = AsyncBlockOperation { (finish) in - self.mapTunnelState(connectionStatus: connectionStatus) { (result) in - switch result { - case .success(let tunnelState): - self.tunnelState = tunnelState - - case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to map the tunnel state") - } - - finish() - } - } - - exclusityController.addOperation(operation, categories: [.stateUpdate]) - } - - /// Maps `NEVPNStatus` to `TunnelState`. - /// Collects the `TunnelConnectionInfo` from the tunnel via IPC if needed before assigning the - /// `tunnelState` - private func mapTunnelState(connectionStatus: NEVPNStatus, completionHandler: @escaping (Result<TunnelState, MapConnectionStatusError>) -> Void) { - switch connectionStatus { - case .connected: - guard let tunnelIpc = tunnelIpc else { - completionHandler(.failure(.missingIpc)) - return - } - - tunnelIpc.getTunnelInformation { (result) in - self.dispatchQueue.async { - let result = result.map { TunnelState.connected($0) } - .mapError { MapConnectionStatusError.ipcRequest($0) } - - completionHandler(result) - } - } - - case .connecting: - completionHandler(.success(.connecting)) - - case .disconnected: - completionHandler(.success(.disconnected)) - - case .disconnecting: - completionHandler(.success(.disconnecting)) - - case .reasserting: - // Refresh the last known public key on reconnect to cover the possibility of - // the key being changed due to key rotation. - if let accountToken = self.accountToken { - switch Self.loadTunnelSettings(accountToken: accountToken) { - case .success(let keychainEntry): - self.tunnelSettings = keychainEntry.tunnelSettings - case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to refresh tunnel settings upon receiving the .reasserting tunnel state.") - } - } - - guard let tunnelIpc = tunnelIpc else { - completionHandler(.failure(.missingIpc)) - return - } - - tunnelIpc.getTunnelInformation { (result) in - self.dispatchQueue.async { - let result = result.map { TunnelState.reconnecting($0) } - .mapError { MapConnectionStatusError.ipcRequest($0) } - - completionHandler(result) - } - } - - case .invalid: - completionHandler(.failure(.invalidConfiguration)) - - @unknown default: - completionHandler(.failure(.unknownStatus(connectionStatus))) - } - } - - private func makeTunnelProvider(accountToken: String, completionHandler: @escaping (Result<TunnelProviderManagerType, TunnelManager.Error>) -> Void) { - TunnelProviderManagerType.loadAllFromPreferences { (tunnels, error) in - self.dispatchQueue.async { - if let error = error { - completionHandler(.failure(.loadAllVPNConfigurations(error))) - } else { - let result = Self.setupTunnelProvider(accountToken: accountToken, tunnels: tunnels) - - guard case .success(let tunnelProvider) = result else { - completionHandler(result) - return - } - - tunnelProvider.saveToPreferences { (error) in - self.dispatchQueue.async { - if let error = error { - completionHandler(.failure(.saveVPNConfiguration(error))) - } else { - // Refresh connection status after saving the tunnel preferences. - // Basically it's only necessary to do for new instances of - // `NETunnelProviderManager`, but we do that for the existing ones too - // for simplicity as it has no side effects. - tunnelProvider.loadFromPreferences { (error) in - self.dispatchQueue.async { - if let error = error { - completionHandler(.failure(.reloadVPNConfiguration(error))) - } else { - completionHandler(.success(tunnelProvider)) - } - } - } - } - } - } - - } - } - } - } - - // MARK: - Private class methods - - private class func loadTunnelSettings(accountToken: String) -> Result<TunnelSettingsManager.KeychainEntry, Error> { - return TunnelSettingsManager.load(searchTerm: .accountToken(accountToken)) - .mapError { Error.readTunnelSettings($0) } - } - - private class func updateTunnelSettings(accountToken: String, block: (inout TunnelSettings) -> Void) -> Result<TunnelSettings, Error> { - return TunnelSettingsManager.update(searchTerm: .accountToken(accountToken), using: block) - .mapError { Error.updateTunnelSettings($0) } - } - - /// Retrieve the existing `TunnelSettings` or create the new ones - private class func makeTunnelSettings(accountToken: String) -> Result<TunnelSettings, TunnelManager.Error> { - return TunnelSettingsManager.load(searchTerm: .accountToken(accountToken)) - .map { $0.tunnelSettings } - .flatMapError { (error) -> Result<TunnelSettings, TunnelManager.Error> in - // Return default tunnel configuration if the config is not found in Keychain - if case .lookupEntry(.itemNotFound) = error { - let defaultConfiguration = TunnelSettings() - - return TunnelSettingsManager - .add(configuration: defaultConfiguration, account: accountToken) - .mapError { .addTunnelSettings($0) } - .map { defaultConfiguration } - } else { - return .failure(.readTunnelSettings(error)) - } - } - } - - private class func setupTunnelProvider(accountToken: String ,tunnels: [TunnelProviderManagerType]?) -> Result<TunnelProviderManagerType, Error> { - // Request persistent keychain reference to tunnel settings - return TunnelSettingsManager.getPersistentKeychainReference(account: accountToken) - .map { (passwordReference) -> TunnelProviderManagerType in - // Get the first available tunnel or make a new one - let tunnelProvider = tunnels?.first ?? TunnelProviderManagerType() - - let protocolConfig = NETunnelProviderProtocol() - protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier - protocolConfig.serverAddress = "" - protocolConfig.username = accountToken - protocolConfig.passwordReference = passwordReference - - tunnelProvider.isEnabled = true - tunnelProvider.localizedDescription = "WireGuard" - tunnelProvider.protocolConfiguration = protocolConfig - - // Enable on-demand VPN, always connect the tunnel when on Wi-Fi or cellular - let alwaysOnRule = NEOnDemandRuleConnect() - alwaysOnRule.interfaceTypeMatch = .any - tunnelProvider.onDemandRules = [alwaysOnRule] - tunnelProvider.isOnDemandEnabled = true - - return tunnelProvider - }.mapError { (error) -> Error in - return .obtainPersistentKeychainReference(error) - } - } - - private func migrateTunnelSettings(accountToken: String) -> Result<Bool, Error> { - let result = TunnelSettingsManager - .migrateKeychainEntry(searchTerm: .accountToken(accountToken)) - .mapError { (error) -> Error in - return .migrateTunnelSettings(error) - } - - switch result { - case .success(let migrated): - if migrated { - self.logger.info("Migrated Keychain tunnel configuration.") - } else { - self.logger.info("Tunnel settings are up to date. No migration needed.") - } - - case .failure(let error): - self.logger.error(chainedError: error) - } - - return result - } - -} diff --git a/ios/MullvadVPN/TunnelManager/AnyTunnelObserver.swift b/ios/MullvadVPN/TunnelManager/AnyTunnelObserver.swift new file mode 100644 index 0000000000..313d190714 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/AnyTunnelObserver.swift @@ -0,0 +1,36 @@ +// +// AnyTunnelObserver.swift +// AnyTunnelObserver +// +// Created by pronebird on 19/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +class AnyTunnelObserver: WeakObserverBox, TunnelObserver { + + typealias Wrapped = TunnelObserver + + private(set) weak var inner: TunnelObserver? + + init<T: TunnelObserver>(_ observer: T) { + inner = observer + } + + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { + inner?.tunnelManager(manager, didUpdateTunnelState: tunnelState) + } + + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) { + inner?.tunnelManager(manager, didUpdateTunnelSettings: tunnelInfo) + } + + func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + inner?.tunnelManager(manager, didFailWithError: error) + } + + static func == (lhs: AnyTunnelObserver, rhs: AnyTunnelObserver) -> Bool { + return lhs.inner === rhs.inner + } +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelInfo.swift b/ios/MullvadVPN/TunnelManager/TunnelInfo.swift new file mode 100644 index 0000000000..5a22468518 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/TunnelInfo.swift @@ -0,0 +1,18 @@ +// +// TunnelInfo.swift +// TunnelInfo +// +// Created by pronebird on 10/09/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Struct that holds current account token and tunnel settings. +struct TunnelInfo { + /// Mullvad account token + var token: String + + /// Tunnel settings + var tunnelSettings: TunnelSettings +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift new file mode 100644 index 0000000000..66cb8dafad --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -0,0 +1,1092 @@ +// +// TunnelManager.swift +// MullvadVPN +// +// Created by pronebird on 25/09/2019. +// Copyright © 2019 Mullvad VPN AB. All rights reserved. +// + +import BackgroundTasks +import Foundation +import NetworkExtension +import UIKit +import Logging +import class WireGuardKit.PublicKey + +/// A class that provides a convenient interface for VPN tunnels configuration, manipulation and +/// monitoring. +class TunnelManager { + /// Private key rotation interval (in seconds) + private static let privateKeyRotationInterval: TimeInterval = 60 * 60 * 24 * 4 + + /// Private key rotation retry interval (in seconds) + private static let privateKeyRotationFailureRetryInterval: TimeInterval = 60 * 15 + + /// Operation categories + private enum OperationCategory { + static let manageTunnelProvider = "TunnelManager.manageTunnelProvider" + static let changeTunnelSettings = "TunnelManager.changeTunnelSettings" + static let notifyTunnelSettingsChange = "TunnelManager.notifyTunnelSettingsChange" + } + + // Switch to stabs on simulator + #if targetEnvironment(simulator) + typealias TunnelProviderManagerType = SimulatorTunnelProviderManager + #else + typealias TunnelProviderManagerType = NETunnelProviderManager + #endif + + static let shared = TunnelManager() + + // MARK: - Internal variables + + private let logger = Logger(label: "TunnelManager") + private let stateQueue = DispatchQueue(label: "TunnelManager.stateQueue") + private let operationQueue: OperationQueue = { + let operationQueue = OperationQueue() + operationQueue.name = "TunnelManager.operationQueue" + return operationQueue + }() + + private var tunnelProvider: TunnelProviderManagerType? + private var ipcSession: TunnelIPC.Session? + private var tunnelConnectionInfoToken: PromiseCancellationToken? + + private let stateLock = NSLock() + private let observerList = ObserverList<AnyTunnelObserver>() + + /// A VPN connection status observer + private var connectionStatusObserver: NSObjectProtocol? + + private(set) var tunnelInfo: TunnelInfo? { + set { + stateLock.withCriticalBlock { + _tunnelInfo = newValue + tunnelInfoDidChange(newValue) + } + } + get { + return stateLock.withCriticalBlock { + return _tunnelInfo + } + } + } + + private var _tunnelInfo: TunnelInfo? + private var _tunnelState = TunnelState.disconnected + + private(set) var tunnelState: TunnelState { + set { + stateLock.withCriticalBlock { + guard _tunnelState != newValue else { return } + + logger.info("Set tunnel state: \(newValue)") + + _tunnelState = newValue + + DispatchQueue.main.async { + self.observerList.forEach { (observer) in + observer.tunnelManager(self, didUpdateTunnelState: newValue) + } + } + } + } + get { + return stateLock.withCriticalBlock { + return _tunnelState + } + } + } + + private init() { + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidBecomeActive), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + } + + // MARK: - Periodic private key rotation + + private var privateKeyRotationTimer: DispatchSourceTimer? + private var isRunningPeriodicPrivateKeyRotation = false + + func startPeriodicPrivateKeyRotation() { + stateQueue.async { + guard !self.isRunningPeriodicPrivateKeyRotation else { return } + + self.logger.debug("Start periodic private key rotation") + + self.isRunningPeriodicPrivateKeyRotation = true + + self.updatePrivateKeyRotationTimer() + } + } + + func stopPeriodicPrivateKeyRotation() { + stateQueue.async { + guard self.isRunningPeriodicPrivateKeyRotation else { return } + + self.logger.debug("Stop periodic private key rotation") + + self.isRunningPeriodicPrivateKeyRotation = false + + self.privateKeyRotationTimer?.cancel() + self.privateKeyRotationTimer = nil + } + } + + private func updatePrivateKeyRotationTimer() { + dispatchPrecondition(condition: .onQueue(stateQueue)) + + guard self.isRunningPeriodicPrivateKeyRotation else { return } + + if let tunnelInfo = self.tunnelInfo { + let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate + let scheduleDate = Date(timeInterval: Self.privateKeyRotationInterval, since: creationDate) + + schedulePrivateKeyRotationTimer(scheduleDate) + } else { + privateKeyRotationTimer?.cancel() + privateKeyRotationTimer = nil + } + } + + /// Schedule new private key rotation timer. + private func schedulePrivateKeyRotationTimer(_ scheduleDate: Date) { + dispatchPrecondition(condition: .onQueue(stateQueue)) + + var cancellationToken: PromiseCancellationToken? + + let timer = DispatchSource.makeTimerSource(flags: [], queue: self.stateQueue) + + timer.setEventHandler { [weak self] in + guard let self = self else { return } + + self.rotatePrivateKey() + .receive(on: self.stateQueue) + .storeCancellationToken(in: &cancellationToken) + .observe { completion in + guard !completion.isCancelled else { return } + + if let scheduleDate = self.handlePrivateKeyRotationCompletion(completion: completion) { + self.schedulePrivateKeyRotationTimer(scheduleDate) + } + } + } + + timer.setCancelHandler { + cancellationToken?.cancel() + } + + // Cancel active timer + privateKeyRotationTimer?.cancel() + + // Assign new timer + privateKeyRotationTimer = timer + + // Schedule and activate + timer.schedule(wallDeadline: .now() + scheduleDate.timeIntervalSinceNow) + timer.activate() + + self.logger.debug("Schedule next private key rotation on \(scheduleDate.logFormatDate())") + } + + // MARK: - Public methods + + /// Initialize the TunnelManager with the tunnel from the system. + /// + /// The given account token is used to ensure that the system tunnel was configured for the same + /// account. The system tunnel is removed in case of inconsistency. + func loadTunnel(accountToken: String?) -> Result<(), TunnelManager.Error>.Promise { + return TunnelProviderManagerType.loadAllFromPreferences() + .receive(on: self.stateQueue) + .mapError { error in + return .loadAllVPNConfigurations(error) + }.mapThen { tunnels in + return Result.Promise { resolver in + self.initializeManager(accountToken: accountToken, tunnels: tunnels) { result in + self.updatePrivateKeyRotationTimer() + resolver.resolve(value: result) + } + } + } + .schedule(on: stateQueue) + .run(on: operationQueue, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings]) + .requestBackgroundTime(taskName: "TunnelManager.loadAccount") + } + + func startTunnel() { + Result<(), TunnelManager.Error>.Promise { resolver in + guard let tunnelInfo = self.tunnelInfo else { + resolver.resolve(value: .failure(.missingAccount)) + return + } + + switch self.tunnelState { + case .disconnecting(.nothing): + self.tunnelState = .disconnecting(.reconnect) + resolver.resolve(value: .success(())) + + case .disconnected, .pendingReconnect: + RelayCache.Tracker.shared.read() + .mapError { error in + return .readRelays(error) + } + .receive(on: self.stateQueue) + .flatMap { cachedRelays in + return RelaySelector.evaluate( + relays: cachedRelays.relays, + constraints: tunnelInfo.tunnelSettings.relayConstraints + ).map { .success($0) } ?? .failure(.cannotSatisfyRelayConstraints) + } + .mapThen { selectorResult in + return self.makeTunnelProvider(accountToken: tunnelInfo.token) + .receive(on: self.stateQueue) + .flatMap { tunnelProvider in + self.setTunnelProvider(tunnelProvider: tunnelProvider) + + var tunnelOptions = PacketTunnelOptions() + + _ = Result { try tunnelOptions.setSelectorResult(selectorResult) } + .mapError { error -> Swift.Error in + self.logger.error(chainedError: AnyChainedError(error), message: "Failed to encode relay selector result.") + return error + } + + self.tunnelState = .connecting(selectorResult.tunnelConnectionInfo) + + return Result { try tunnelProvider.connection.startVPNTunnel(options: tunnelOptions.rawOptions()) } + .mapError { error in + return .startVPNTunnel(error) + } + } + }.observe { completion in + resolver.resolve(completion: completion) + } + + default: + // Do not attempt to start the tunnel in all other cases. + resolver.resolve(value: .success(())) + } + } + .schedule(on: stateQueue) + .run(on: operationQueue, categories: [OperationCategory.manageTunnelProvider]) + .requestBackgroundTime(taskName: "TunnelManager.startTunnel") + .onFailure { error in + self.sendFailureToObservers(error) + } + .observe { _ in } + } + + func stopTunnel() { + Result<(), Error>.Promise { resolver in + guard let tunnelProvider = self.tunnelProvider else { + resolver.resolve(value: .failure(.missingAccount)) + return + } + + switch self.tunnelState { + case .disconnecting(.reconnect): + self.tunnelState = .disconnecting(.nothing) + resolver.resolve(value: .success(())) + + case .connected, .connecting: + // Disable on-demand when stopping the tunnel to prevent it from coming back up + tunnelProvider.isOnDemandEnabled = false + + tunnelProvider.saveToPreferences() + .mapError { error in + return Error.saveVPNConfiguration(error) + } + .observe { completion in + tunnelProvider.connection.stopVPNTunnel() + resolver.resolve(completion: completion) + } + + default: + resolver.resolve(value: .success(())) + } + } + .schedule(on: stateQueue) + .run(on: operationQueue, categories: [OperationCategory.manageTunnelProvider]) + .requestBackgroundTime(taskName: "TunnelManager.stopTunnel") + .onFailure { error in + self.sendFailureToObservers(error) + } + .observe { _ in } + } + + func reconnectTunnel() { + notifyTunnelOnSettingsChange().observe { _ in } + } + + func setAccount(accountToken: String) -> Result<(), TunnelManager.Error>.Promise { + return Promise.deferred { Self.makeTunnelSettings(accountToken: accountToken) } + .mapThen { tunnelSettings -> Result<TunnelSettings, Error>.Promise in + let interfaceSettings = tunnelSettings.interface + guard interfaceSettings.addresses.isEmpty else { + return .success(tunnelSettings) + } + + // Push wireguard key if addresses were not received yet + return self.pushWireguardKeyAndUpdateSettings(accountToken: accountToken, publicKey: interfaceSettings.publicKey) + } + .receive(on: self.stateQueue) + .onSuccess { tunnelSettings in + self.tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: tunnelSettings) + self.updatePrivateKeyRotationTimer() + } + .setOutput(()) + .schedule(on: stateQueue) + .run(on: operationQueue, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings]) + .requestBackgroundTime(taskName: "TunnelManager.setAccount") + } + + /// Remove the account token and remove the active tunnel + func unsetAccount() -> Result<(), TunnelManager.Error>.Promise { + return Promise.deferred { self.tunnelInfo } + .some(or: Error.missingAccount) + .mapThen { tunnelInfo in + let publicKey = tunnelInfo.tunnelSettings.interface.publicKey + + return self.removeWireguardKeyFromServer(accountToken: tunnelInfo.token, publicKey: publicKey) + .receive(on: self.stateQueue) + .then { result -> Result<(), Error>.Promise in + switch result { + case .success(let isRemoved): + self.logger.warning("Removed the WireGuard key from server: \(isRemoved)") + + case .failure(let error): + self.logger.error(chainedError: error, message: "Unset account error") + } + + // Unregister from receiving the tunnel state changes + self.unregisterConnectionObserver() + self.tunnelConnectionInfoToken = nil + self.tunnelState = .disconnected + self.ipcSession = nil + + // Remove settings from Keychain + if case .failure(let error) = TunnelSettingsManager.remove(searchTerm: .accountToken(tunnelInfo.token)) { + // Ignore Keychain errors because that normally means that the Keychain + // configuration was already removed and we shouldn't be blocking the + // user from logging out + self.logger.error( + chainedError: error, + message: "Failure to remove tunnel setting from keychain when unsetting user account" + ) + } + + self.tunnelInfo = nil + self.updatePrivateKeyRotationTimer() + + guard let tunnelProvider = self.tunnelProvider else { + return .success(()) + } + + self.tunnelProvider = nil + + // Remove VPN configuration + return tunnelProvider.removeFromPreferences() + .flatMapError { error -> Result<(), Error> in + // Ignore error but log it + self.logger.error( + chainedError: Error.removeVPNConfiguration(error), + message: "Failure to remove system VPN configuration when unsetting user account." + ) + + return .success(()) + } + } + } + .schedule(on: stateQueue) + .run(on: operationQueue, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings]) + .requestBackgroundTime(taskName: "TunnelManager.unsetAccount") + } + + func regeneratePrivateKey() -> Result<(), TunnelManager.Error>.Promise { + return Promise.deferred { self.tunnelInfo } + .some(or: .missingAccount) + .mapThen { tunnelInfo in + let newPrivateKey = PrivateKeyWithMetadata() + let oldPublicKeyMetadata = tunnelInfo.tunnelSettings.interface + .privateKey + .publicKeyWithMetadata + + return self.replaceWireguardKeyAndUpdateSettings( + accountToken: tunnelInfo.token, + oldPublicKey: oldPublicKeyMetadata, + newPrivateKey: newPrivateKey + ).onSuccess { newTunnelSettings in + self.tunnelInfo?.tunnelSettings = newTunnelSettings + self.updatePrivateKeyRotationTimer() + + self.notifyTunnelOnSettingsChange().observe { _ in } + } + .setOutput(()) + } + .schedule(on: stateQueue) + .run(on: operationQueue, categories: [OperationCategory.changeTunnelSettings]) + .requestBackgroundTime(taskName: "TunnelManager.regeneratePrivateKey") + } + + func rotatePrivateKey() -> Result<KeyRotationResult, TunnelManager.Error>.Promise { + return Promise.deferred { self.tunnelInfo } + .some(or: .missingAccount) + .mapThen { tunnelInfo in + let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate + let timeInterval = Date().timeIntervalSince(creationDate) + + guard timeInterval >= Self.privateKeyRotationInterval else { + return .success(.throttled(creationDate)) + } + + let newPrivateKey = PrivateKeyWithMetadata() + let oldPublicKeyMetadata = tunnelInfo.tunnelSettings.interface + .privateKey + .publicKeyWithMetadata + + return self.replaceWireguardKeyAndUpdateSettings(accountToken: tunnelInfo.token, oldPublicKey: oldPublicKeyMetadata, newPrivateKey: newPrivateKey) + .onSuccess { newTunnelSettings in + self.tunnelInfo?.tunnelSettings = newTunnelSettings + } + .mapThen { _ in + return self.notifyTunnelOnSettingsChange().then { _ in + return .success(.finished) + } + } + } + .schedule(on: stateQueue) + .run(on: operationQueue, categories: [OperationCategory.changeTunnelSettings]) + .requestBackgroundTime(taskName: "TunnelManager.rotatePrivateKey") + } + + func setRelayConstraints(_ newConstraints: RelayConstraints) -> Result<(), TunnelManager.Error>.Promise { + return Promise.deferred { self.tunnelInfo } + .some(or: .missingAccount) + .flatMap { tunnelInfo in + return Self.updateTunnelSettings(accountToken: tunnelInfo.token) { tunnelSettings in + tunnelSettings.relayConstraints = newConstraints + } + } + .onSuccess { newTunnelSettings in + self.tunnelInfo?.tunnelSettings = newTunnelSettings + self.notifyTunnelOnSettingsChange().observe { _ in } + } + .setOutput(()) + .schedule(on: stateQueue) + .run(on: operationQueue, categories: [OperationCategory.changeTunnelSettings]) + .requestBackgroundTime(taskName: "TunnelManager.setRelayConstraints") + } + + func setDNSSettings(_ newDNSSettings: DNSSettings) -> Result<(), TunnelManager.Error>.Promise { + return Promise.deferred { self.tunnelInfo } + .some(or: .missingAccount) + .flatMap { tunnelInfo in + return Self.updateTunnelSettings(accountToken: tunnelInfo.token) { tunnelSettings in + tunnelSettings.interface.dnsSettings = newDNSSettings + } + } + .onSuccess { newTunnelSettings in + self.tunnelInfo?.tunnelSettings = newTunnelSettings + self.notifyTunnelOnSettingsChange().observe { _ in } + } + .setOutput(()) + .schedule(on: stateQueue) + .run(on: operationQueue, categories: [OperationCategory.changeTunnelSettings]) + .requestBackgroundTime(taskName: "TunnelManager.setDNSSettings") + } + + // MARK: - Tunnel observeration + + /// Add tunnel observer. + /// In order to cancel the observation, either call `removeTunnelObserver(_:)` or simply release + /// the observer. + func addObserver<T: TunnelObserver>(_ observer: T) { + observerList.append(AnyTunnelObserver(observer)) + } + + /// Remove tunnel observer. + func removeObserver<T: TunnelObserver>(_ observer: T) { + observerList.remove(AnyTunnelObserver(observer)) + } + + // MARK: - Private methods + + private func tunnelInfoDidChange(_ newTunnelInfo: TunnelInfo?) { + // Notify observers + DispatchQueue.main.async { + self.observerList.forEach { (observer) in + observer.tunnelManager(self, didUpdateTunnelSettings: newTunnelInfo) + } + } + } + + private func initializeManager(accountToken: String?, tunnels: [TunnelProviderManagerType]?, completionHandler: @escaping (Result<(), Error>) -> Void) { + // Migrate the tunnel settings if needed + let migrationResult = accountToken.map { self.migrateTunnelSettings(accountToken: $0) } + switch migrationResult { + case .success, .none: + break + case .failure(let migrationError): + completionHandler(.failure(migrationError)) + return + } + + switch (tunnels?.first, accountToken) { + // Case 1: tunnel exists and account token is set. + // Verify that tunnel can access the configuration via the persistent keychain reference + // stored in `passwordReference` field of VPN configuration. + case (.some(let tunnelProvider), .some(let accountToken)): + let verificationResult = self.verifyTunnel(tunnelProvider: tunnelProvider, expectedAccountToken: accountToken) + let tunnelSettingsResult = Self.loadTunnelSettings(accountToken: accountToken) + + switch (verificationResult, tunnelSettingsResult) { + case (.success(true), .success(let keychainEntry)): + self.tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: keychainEntry.tunnelSettings) + self.setTunnelProvider(tunnelProvider: tunnelProvider) + + completionHandler(.success(())) + + // Remove the tunnel when failed to verify it but successfuly loaded the tunnel + // settings. + case (.failure(let verificationError), .success(let keychainEntry)): + self.logger.error(chainedError: verificationError, message: "Failed to verify the tunnel but successfully loaded the tunnel settings. Removing the tunnel.") + + // Identical code path as the case below. + fallthrough + + // Remove the tunnel with corrupt configuration. + // It will be re-created upon the first attempt to connect the tunnel. + case (.success(false), .success(let keychainEntry)): + tunnelProvider.removeFromPreferences() + .receive(on: self.stateQueue) + .mapError { error in + return .removeInconsistentVPNConfiguration(error) + } + .onSuccess { _ in + self.tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: keychainEntry.tunnelSettings) + } + .observe { completion in + completionHandler(completion.unwrappedValue!) + } + + // Remove the tunnel when failed to verify the tunnel and load tunnel settings. + case (.failure(let verificationError), .failure(_)): + self.logger.error(chainedError: verificationError, message: "Failed to verify the tunnel and load tunnel settings. Removing the tunnel.") + + tunnelProvider.removeFromPreferences() + .mapError { error in + return .removeInconsistentVPNConfiguration(error) + } + .flatMap { _ in + return .failure(verificationError) + } + .observe { completion in + completionHandler(completion.unwrappedValue!) + } + + // Remove the tunnel when the app is not able to read tunnel settings + case (.success(_), .failure(let settingsReadError)): + self.logger.error(chainedError: settingsReadError, message: "Failed to load tunnel settings. Removing the tunnel.") + + tunnelProvider.removeFromPreferences() + .mapError { error in + return .removeInconsistentVPNConfiguration(error) + } + .flatMap { _ in + return .failure(settingsReadError) + } + .observe { completion in + completionHandler(completion.unwrappedValue!) + } + } + + // Case 2: tunnel exists but account token is unset. + // Remove the orphaned tunnel. + case (.some(let tunnelProvider), .none): + tunnelProvider.removeFromPreferences() + .mapError { error in + return .removeInconsistentVPNConfiguration(error) + } + .observe { completion in + completionHandler(completion.unwrappedValue!) + } + + // Case 3: tunnel does not exist but the account token is set. + // Verify that tunnel settings exists in keychain. + case (.none, .some(let accountToken)): + switch Self.loadTunnelSettings(accountToken: accountToken) { + case .success(let keychainEntry): + self.tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: keychainEntry.tunnelSettings) + + completionHandler(.success(())) + + case .failure(let error): + completionHandler(.failure(error)) + } + + // Case 4: no tunnels exist and account token is unset. + case (.none, .none): + completionHandler(.success(())) + } + } + + private func verifyTunnel(tunnelProvider: TunnelProviderManagerType, expectedAccountToken accountToken: String) -> Result<Bool, Error> { + // Check that the VPN configuration points to the same account token + guard let username = tunnelProvider.protocolConfiguration?.username, username == accountToken else { + logger.warning("The token assigned to the VPN configuration does not match the logged in account.") + return .success(false) + } + + // Check that the passwordReference, containing the keychain reference for tunnel + // configuration, is set. + guard let keychainReference = tunnelProvider.protocolConfiguration?.passwordReference else { + logger.warning("VPN configuration is missing the passwordReference.") + return .success(false) + } + + // Verify that the keychain reference points to the existing entry in Keychain. + // Bad reference is possible when migrating the user data from one device to the other. + return TunnelSettingsManager.exists(searchTerm: .persistentReference(keychainReference)) + .mapError { (error) -> Error in + logger.error(chainedError: error, message: "Failed to verify the persistent keychain reference for tunnel settings.") + + return Error.readTunnelSettings(error) + } + } + + /// Set the instance of the active tunnel and add the tunnel status observer + private func setTunnelProvider(tunnelProvider: TunnelProviderManagerType) { + guard self.tunnelProvider != tunnelProvider else { + return + } + + // Save the new active tunnel provider + self.tunnelProvider = tunnelProvider + + // Set up tunnel IPC + self.ipcSession = TunnelIPC.Session(from: tunnelProvider) + + // Register for tunnel connection status changes + unregisterConnectionObserver() + connectionStatusObserver = NotificationCenter.default + .addObserver(forName: .NEVPNStatusDidChange, object: tunnelProvider.connection, queue: nil) { + [weak self] (notification) in + guard let self = self else { return } + + self.stateQueue.async { + self.updateTunnelState() + } + } + + // Update the existing state + updateTunnelState() + } + + private func unregisterConnectionObserver() { + if let connectionStatusObserver = connectionStatusObserver { + NotificationCenter.default.removeObserver(connectionStatusObserver) + self.connectionStatusObserver = nil + } + } + + private func pushWireguardKeyAndUpdateSettings(accountToken: String, publicKey: PublicKey) -> Result<TunnelSettings, Error>.Promise { + return REST.Client.shared.pushWireguardKey(token: accountToken, publicKey: publicKey) + .mapError { error in + return .pushWireguardKey(error) + } + .receive(on: stateQueue) + .flatMap { associatedAddresses in + return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in + tunnelSettings.interface.addresses = [ + associatedAddresses.ipv4Address, + associatedAddresses.ipv6Address + ] + } + } + } + + private func removeWireguardKeyFromServer(accountToken: String, publicKey: PublicKey) -> Result<Bool, Error>.Promise { + return REST.Client.shared.deleteWireguardKey(token: accountToken, publicKey: publicKey) + .map { _ in + return true + } + .flatMapError { restError -> Result<Bool, Error> in + if case .server(.pubKeyNotFound) = restError { + return .success(false) + } else { + return .failure(.removeWireguardKey(restError)) + } + } + } + + private func replaceWireguardKeyAndUpdateSettings( + accountToken: String, + oldPublicKey: PublicKeyWithMetadata, + newPrivateKey: PrivateKeyWithMetadata + ) -> Result<TunnelSettings, Error>.Promise + { + return REST.Client.shared.replaceWireguardKey(token: accountToken, oldPublicKey: oldPublicKey.publicKey, newPublicKey: newPrivateKey.publicKey) + .mapError { error in + return .replaceWireguardKey(error) + } + .receive(on: self.stateQueue) + .flatMap { associatedAddresses in + return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in + tunnelSettings.interface.privateKey = newPrivateKey + tunnelSettings.interface.addresses = [ + associatedAddresses.ipv4Address, + associatedAddresses.ipv6Address + ] + } + } + } + + /// Update `TunnelState` from `NEVPNStatus`. + /// Collects the `TunnelConnectionInfo` from the tunnel via IPC if needed before assigning the `tunnelState` + private func updateTunnelState() { + dispatchPrecondition(condition: .onQueue(stateQueue)) + + guard let connectionStatus = self.tunnelProvider?.connection.status else { return } + + logger.debug("VPN status changed to \(connectionStatus)") + tunnelConnectionInfoToken = nil + + switch connectionStatus { + case .connecting: + switch tunnelState { + case .connecting(.some(_)): + logger.debug("Ignore repeating connecting state.") + default: + tunnelState = .connecting(nil) + } + + case .reasserting: + ipcSession?.getTunnelConnectionInfo() + .receive(on: stateQueue) + .storeCancellationToken(in: &tunnelConnectionInfoToken) + .onSuccess { connectionInfo in + if let connectionInfo = connectionInfo { + self.tunnelState = .reconnecting(connectionInfo) + } + } + .observe { _ in } + + case .connected: + ipcSession?.getTunnelConnectionInfo() + .receive(on: stateQueue) + .storeCancellationToken(in: &tunnelConnectionInfoToken) + .onSuccess { connectionInfo in + if let connectionInfo = connectionInfo { + self.tunnelState = .connected(connectionInfo) + } + } + .observe { _ in } + + case .disconnected: + switch tunnelState { + case .pendingReconnect: + logger.debug("Ignore disconnected state when pending reconnect.") + + case .disconnecting(.reconnect): + logger.debug("Restart the tunnel on disconnect.") + tunnelState = .pendingReconnect + startTunnel() + + default: + tunnelState = .disconnected + } + + case .disconnecting: + switch tunnelState { + case .disconnecting: + break + default: + tunnelState = .disconnecting(.nothing) + } + + case .invalid: + tunnelState = .disconnected + + @unknown default: + logger.debug("Unknown NEVPNStatus: \(connectionStatus.rawValue)") + } + } + + private func makeTunnelProvider(accountToken: String) -> Result<TunnelProviderManagerType, Error>.Promise { + return TunnelProviderManagerType.loadAllFromPreferences() + .mapError { error -> Error in + return .loadAllVPNConfigurations(error) + } + .flatMap { tunnels in + return Self.setupTunnelProvider(accountToken: accountToken, tunnels: tunnels) + } + .mapThen { tunnelProvider in + return tunnelProvider.saveToPreferences() + .mapError { error in + return .saveVPNConfiguration(error) + } + .mapThen { _ in + // Refresh connection status after saving the tunnel preferences. + // Basically it's only necessary to do for new instances of + // `NETunnelProviderManager`, but we do that for the existing ones too + // for simplicity as it has no side effects. + return tunnelProvider.loadFromPreferences() + .mapError { error in + return .reloadVPNConfiguration(error) + } + } + .setOutput(tunnelProvider) + } + } + + private func sendFailureToObservers(_ failure: Error) { + DispatchQueue.main.async { + self.observerList.forEach { observer in + observer.tunnelManager(self, didFailWithError: failure) + } + } + } + + private func notifyTunnelOnSettingsChange() -> Promise<Void> { + return Promise.deferred { () -> (TunnelIPC.Session, TunnelProviderManagerType)? in + if let ipcSession = self.ipcSession, let tunnelProvider = self.tunnelProvider { + return (ipcSession, tunnelProvider) + } else { + return nil + } + } + .mapThen(defaultValue: ()) { (ipc, tunnelProvider) in + return Promise { resolver in + let connection = tunnelProvider.connection + var statusObserver: NSObjectProtocol? + var ipcToken: PromiseCancellationToken? + + let releaseObserver = { + if let statusObserver = statusObserver { + NotificationCenter.default.removeObserver(statusObserver) + } + } + + let handleStatus = { + switch connection.status { + case .connected: + releaseObserver() + + ipc.reloadTunnelSettings() + .storeCancellationToken(in: &ipcToken) + .observe { completion in + switch completion { + case .finished(let result): + if case .failure(let error) = result { + self.logger.error(chainedError: error, message: "Failed to send IPC request to reload tunnel settings") + } + resolver.resolve(value: ()) + case .cancelled: + resolver.resolve(completion: .cancelled) + } + } + + case .connecting, .reasserting: + // wait for transition to complete + break + + case .invalid, .disconnecting, .disconnected: + releaseObserver() + resolver.resolve(value: ()) + + @unknown default: + break + } + } + + // Add connection status observer + statusObserver = NotificationCenter.default.addObserver( + forName: .NEVPNStatusDidChange, + object: connection, + queue: .main) { note in + handleStatus() + } + + // Set cancellation handler + resolver.setCancelHandler { + DispatchQueue.main.async { + releaseObserver() + ipcToken = nil + } + } + + // Run initial check + DispatchQueue.main.async { + handleStatus() + } + } + } + .schedule(on: stateQueue) + .run(on: operationQueue, categories: [OperationCategory.notifyTunnelSettingsChange]) + .requestBackgroundTime(taskName: "TunnelManager.notifyTunnelOnSettingsChange") + } + + @objc private func applicationDidBecomeActive() { + stateQueue.async { + // Refresh tunnel state when application becomes active. + self.updateTunnelState() + } + } + + // MARK: - Private class methods + + private class func loadTunnelSettings(accountToken: String) -> Result<TunnelSettingsManager.KeychainEntry, Error> { + return TunnelSettingsManager.load(searchTerm: .accountToken(accountToken)) + .mapError { Error.readTunnelSettings($0) } + } + + private class func updateTunnelSettings(accountToken: String, block: (inout TunnelSettings) -> Void) -> Result<TunnelSettings, Error> { + return TunnelSettingsManager.update(searchTerm: .accountToken(accountToken), using: block) + .mapError { Error.updateTunnelSettings($0) } + } + + /// Retrieve the existing `TunnelSettings` or create the new ones + private class func makeTunnelSettings(accountToken: String) -> Result<TunnelSettings, Error> { + return Self.loadTunnelSettings(accountToken: accountToken) + .map { $0.tunnelSettings } + .flatMapError { error in + if case .readTunnelSettings(.lookupEntry(.itemNotFound)) = error { + let defaultConfiguration = TunnelSettings() + + return TunnelSettingsManager + .add(configuration: defaultConfiguration, account: accountToken) + .mapError { .addTunnelSettings($0) } + .map { defaultConfiguration } + } else { + return .failure(error) + } + } + } + + private class func setupTunnelProvider(accountToken: String ,tunnels: [TunnelProviderManagerType]?) -> Result<TunnelProviderManagerType, Error> { + // Request persistent keychain reference to tunnel settings + return TunnelSettingsManager.getPersistentKeychainReference(account: accountToken) + .map { (passwordReference) -> TunnelProviderManagerType in + // Get the first available tunnel or make a new one + let tunnelProvider = tunnels?.first ?? TunnelProviderManagerType() + + let protocolConfig = NETunnelProviderProtocol() + protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier + protocolConfig.serverAddress = "" + protocolConfig.username = accountToken + protocolConfig.passwordReference = passwordReference + + tunnelProvider.isEnabled = true + tunnelProvider.localizedDescription = "WireGuard" + tunnelProvider.protocolConfiguration = protocolConfig + + // Enable on-demand VPN, always connect the tunnel when on Wi-Fi or cellular + let alwaysOnRule = NEOnDemandRuleConnect() + alwaysOnRule.interfaceTypeMatch = .any + tunnelProvider.onDemandRules = [alwaysOnRule] + tunnelProvider.isOnDemandEnabled = true + + return tunnelProvider + }.mapError { (error) -> Error in + return .obtainPersistentKeychainReference(error) + } + } + + private func migrateTunnelSettings(accountToken: String) -> Result<Bool, Error> { + let result = TunnelSettingsManager + .migrateKeychainEntry(searchTerm: .accountToken(accountToken)) + .mapError { (error) -> Error in + return .migrateTunnelSettings(error) + } + + switch result { + case .success(let migrated): + if migrated { + self.logger.info("Migrated Keychain tunnel configuration.") + } else { + self.logger.info("Tunnel settings are up to date. No migration needed.") + } + + case .failure(let error): + self.logger.error(chainedError: error) + } + + return result + } + +} + +extension TunnelManager { + /// Key rotation result. + enum KeyRotationResult: CustomStringConvertible { + /// Request to rotate the key was throttled. + case throttled(_ lastKeyCreationDate: Date) + + /// New key was generated. + case finished + + var description: String { + switch self { + case .throttled: + return "throttled" + case .finished: + return "finished" + } + } + } +} + +extension TunnelManager { + fileprivate func handlePrivateKeyRotationCompletion(completion: PromiseCompletion<Result<KeyRotationResult, TunnelManager.Error>>) -> Date? { + switch completion { + case .finished(.success(let result)): + switch result { + case .finished: + self.logger.debug("Finished private key rotation") + case .throttled: + self.logger.debug("Private key was already rotated earlier") + } + + return self.nextScheduleDate(result) + + case .finished(.failure(let error)): + self.logger.error(chainedError: error, message: "Failed to rotate private key in background task") + + return self.nextRetryScheduleDate(error) + + case .cancelled: + self.logger.debug("Private key rotation was cancelled") + + return Date(timeIntervalSinceNow: Self.privateKeyRotationFailureRetryInterval) + } + } + + fileprivate func nextScheduleDate(_ result: KeyRotationResult) -> Date { + switch result { + case .finished: + return Date(timeIntervalSinceNow: Self.privateKeyRotationInterval) + + case .throttled(let lastKeyCreationDate): + return Date(timeInterval: Self.privateKeyRotationInterval, since: lastKeyCreationDate) + } + } + + fileprivate func nextRetryScheduleDate(_ error: TunnelManager.Error) -> Date? { + switch error { + case .missingAccount: + // Do not retry if logged out. + return nil + + case .replaceWireguardKey(.server(.invalidAccount)): + // Do not retry if account was removed. + return nil + + default: + return Date(timeIntervalSinceNow: Self.privateKeyRotationFailureRetryInterval) + } + } +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift new file mode 100644 index 0000000000..f282b83f8b --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift @@ -0,0 +1,115 @@ +// +// TunnelManagerError.swift +// TunnelManagerError +// +// Created by pronebird on 07/09/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension TunnelManager { + /// An error emitted by all public methods of TunnelManager + enum Error: ChainedError { + /// Account token is not set + case missingAccount + + /// A failure to start the VPN tunnel via system call + case startVPNTunnel(Swift.Error) + + /// A failure to load the system VPN configurations created by the app + case loadAllVPNConfigurations(Swift.Error) + + /// A failure to save the system VPN configuration + case saveVPNConfiguration(Swift.Error) + + /// A failure to reload the system VPN configuration + case reloadVPNConfiguration(Swift.Error) + + /// A failure to remove the system VPN configuration + case removeVPNConfiguration(Swift.Error) + + /// A failure to perform a recovery (by removing the VPN configuration) when a corrupt + /// VPN configuration is detected. + case removeInconsistentVPNConfiguration(Swift.Error) + + /// A failure to read tunnel settings + case readTunnelSettings(TunnelSettingsManager.Error) + + /// A failure to read relays cache + case readRelays(RelayCache.Error) + + /// A failure to find a relay satisfying the given constraints + case cannotSatisfyRelayConstraints + + /// A failure to add the tunnel settings + case addTunnelSettings(TunnelSettingsManager.Error) + + /// A failure to update the tunnel settings + case updateTunnelSettings(TunnelSettingsManager.Error) + + /// A failure to remove the tunnel settings from Keychain + case removeTunnelSettings(TunnelSettingsManager.Error) + + /// A failure to migrate tunnel settings + case migrateTunnelSettings(TunnelSettingsManager.Error) + + /// Unable to obtain the persistent keychain reference for the tunnel settings + case obtainPersistentKeychainReference(TunnelSettingsManager.Error) + + /// A failure to push the public WireGuard key + case pushWireguardKey(REST.Error) + + /// A failure to replace the public WireGuard key + case replaceWireguardKey(REST.Error) + + /// A failure to remove the public WireGuard key + case removeWireguardKey(REST.Error) + + /// A failure to schedule background task + case backgroundTaskScheduler(Swift.Error) + + var errorDescription: String? { + switch self { + case .missingAccount: + return "Missing account token" + case .startVPNTunnel: + return "Failed to start the VPN tunnel" + case .loadAllVPNConfigurations: + return "Failed to load the system VPN configurations" + case .saveVPNConfiguration: + return "Failed to save the system VPN configuration" + case .reloadVPNConfiguration: + return "Failed to reload the system VPN configuration" + case .removeVPNConfiguration: + return "Failed to remove the system VPN configuration" + case .removeInconsistentVPNConfiguration: + return "Failed to remove the inconsistent VPN tunnel" + case .readTunnelSettings: + return "Failed to read the tunnel settings" + case .readRelays: + return "Failed to read relays" + case .cannotSatisfyRelayConstraints: + return "Failed to satisfy the relay constraints" + case .addTunnelSettings: + return "Failed to add the tunnel settings" + case .updateTunnelSettings: + return "Failed to update the tunnel settings" + case .removeTunnelSettings: + return "Failed to remove the tunnel settings" + case .migrateTunnelSettings: + return "Failed to migrate the tunnel settings" + case .obtainPersistentKeychainReference: + return "Failed to obtain the persistent keychain reference" + case .pushWireguardKey: + return "Failed to push the WireGuard key to server" + case .replaceWireguardKey: + return "Failed to replace the WireGuard key on server" + case .removeWireguardKey: + return "Failed to remove the WireGuard key from server" + case .backgroundTaskScheduler: + return "Failed to schedule background task" + } + } + } +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift new file mode 100644 index 0000000000..5fefb4a89e --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift @@ -0,0 +1,15 @@ +// +// TunnelObserver.swift +// TunnelObserver +// +// Created by pronebird on 19/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol TunnelObserver: AnyObject { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) + func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift new file mode 100644 index 0000000000..b016144dd3 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift @@ -0,0 +1,70 @@ +// +// TunnelState.swift +// TunnelState +// +// Created by pronebird on 11/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// A enum that describes the tunnel state +enum TunnelState: Equatable, CustomStringConvertible { + /// Pending reconnect after disconnect. + case pendingReconnect + + /// Connecting the tunnel. Contains the pending action carried over from disconnected state. + case connecting(TunnelConnectionInfo?) + + /// Connected the tunnel + case connected(TunnelConnectionInfo) + + /// Disconnecting the tunnel + case disconnecting(ActionAfterDisconnect) + + /// Disconnected the tunnel + case disconnected + + /// Reconnecting the tunnel. Normally this state appears in response to changing the + /// relay constraints and asking the running tunnel to reload the configuration. + case reconnecting(TunnelConnectionInfo) + + var description: String { + switch self { + case .pendingReconnect: + return "pending reconnect after disconnect" + case .connecting(let connectionInfo): + if let connectionInfo = connectionInfo { + return "connecting to \(connectionInfo.hostname)" + } else { + return "connecting, fetching relay" + } + case .connected(let connectionInfo): + return "connected to \(connectionInfo.hostname)" + case .disconnecting(let actionAfterDisconnect): + return "disconnecting and then \(actionAfterDisconnect)" + case .disconnected: + return "disconnected" + case .reconnecting(let connectionInfo): + return "reconnecting to \(connectionInfo.hostname)" + } + } +} + +/// A enum that describes the action to perform after disconnect +enum ActionAfterDisconnect { + /// Do nothing after disconnecting + case nothing + + /// Reconnect after disconnecting + case reconnect + + var description: String { + switch self { + case .nothing: + return "do nothing" + case .reconnect: + return "reconnect" + } + } +} diff --git a/ios/MullvadVPN/TunnelSettings.swift b/ios/MullvadVPN/TunnelSettings.swift index a2a6b24029..c8503c894d 100644 --- a/ios/MullvadVPN/TunnelSettings.swift +++ b/ios/MullvadVPN/TunnelSettings.swift @@ -7,9 +7,8 @@ // import Foundation -import Network -import NetworkExtension -import WireGuardKit +import class WireGuardKit.PublicKey +import struct WireGuardKit.IPAddressRange /// A struct that holds a tun interface configuration. struct InterfaceSettings: Codable, Equatable { @@ -17,6 +16,10 @@ struct InterfaceSettings: Codable, Equatable { var addresses: [IPAddressRange] var dnsSettings: DNSSettings + var publicKey: PublicKey { + return privateKey.publicKeyWithMetadata.publicKey + } + private enum CodingKeys: String, CodingKey { case privateKey, addresses, dnsSettings } diff --git a/ios/MullvadVPN/WireguardAssociatedAddresses.swift b/ios/MullvadVPN/WireguardAssociatedAddresses.swift index bcf14cdd86..edd8d55ca0 100644 --- a/ios/MullvadVPN/WireguardAssociatedAddresses.swift +++ b/ios/MullvadVPN/WireguardAssociatedAddresses.swift @@ -8,7 +8,7 @@ import Foundation import Network -import WireGuardKit +import struct WireGuardKit.IPAddressRange struct WireguardAssociatedAddresses: Codable { let ipv4Address: IPAddressRange diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift index d5705582ec..ee490a3a6d 100644 --- a/ios/MullvadVPN/WireguardKeysViewController.swift +++ b/ios/MullvadVPN/WireguardKeysViewController.swift @@ -33,7 +33,8 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { }() private var publicKeyPeriodicUpdateTimer: DispatchSourceTimer? - private var copyToPasteboardWork: DispatchWorkItem? + private var copyToPasteboardCancellationToken: PromiseCancellationToken? + private var verifyKeyCancellationToken: PromiseCancellationToken? private let alertPresenter = AlertPresenter() private let logger = Logger(label: "WireguardKeys") @@ -77,7 +78,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { contentView.verifyKeyButton.addTarget(self, action: #selector(handleVerifyKey(_:)), for: .touchUpInside) TunnelManager.shared.addObserver(self) - updatePublicKey(tunnelSettings: TunnelManager.shared.tunnelSettings, animated: false) + updatePublicKey(tunnelSettings: TunnelManager.shared.tunnelInfo?.tunnelSettings, animated: false) startPublicKeyPeriodicUpdate() } @@ -86,7 +87,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { let interval = DispatchTimeInterval.seconds(kCreationDateRefreshInterval) let timerSource = DispatchSource.makeTimerSource(queue: .main) timerSource.setEventHandler { [weak self] () -> Void in - self?.updatePublicKey(tunnelSettings: TunnelManager.shared.tunnelSettings, animated: true) + self?.updatePublicKey(tunnelSettings: TunnelManager.shared.tunnelInfo?.tunnelSettings, animated: true) } timerSource.schedule(deadline: .now() + interval, repeating: interval) timerSource.activate() @@ -96,20 +97,24 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { // MARK: - TunnelObserver - func tunnelStateDidChange(tunnelState: TunnelState) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { // no-op } - func tunnelSettingsDidChange(tunnelSettings: TunnelSettings?) { - DispatchQueue.main.async { - self.updatePublicKey(tunnelSettings: tunnelSettings, animated: true) - } + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) { + self.updatePublicKey(tunnelSettings: tunnelInfo?.tunnelSettings, animated: true) + } + + func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + // no-op } // MARK: - Actions private func copyPublicKey() { - guard let metadata = TunnelManager.shared.tunnelSettings?.interface.privateKey.publicKeyWithMetadata else { return } + guard let tunnelInfo = TunnelManager.shared.tunnelInfo else { return } + + let metadata = tunnelInfo.tunnelSettings.interface.privateKey.publicKeyWithMetadata UIPasteboard.general.string = metadata.stringRepresentation() @@ -117,14 +122,14 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { string: NSLocalizedString("COPIED_TO_PASTEBOARD_LABEL", tableName: "WireguardKeys", comment: ""), animated: true) - let dispatchWork = DispatchWorkItem { [weak self] in - self?.updatePublicKey(tunnelSettings: TunnelManager.shared.tunnelSettings, animated: true) - } - - DispatchQueue.main.asyncAfter(wallDeadline: .now() + .seconds(3), execute: dispatchWork) + Promise.deferred { TunnelManager.shared.tunnelInfo?.tunnelSettings } + .delay(by: .seconds(3), timerType: .walltime, queue: .main) + .storeCancellationToken(in: ©ToPasteboardCancellationToken) + .observe { [weak self] completion in + guard let tunnelSettings = completion.unwrappedValue else { return } - self.copyToPasteboardWork?.cancel() - self.copyToPasteboardWork = dispatchWork + self?.updatePublicKey(tunnelSettings: tunnelSettings, animated: true) + } } @objc private func handleRegenerateKey(_ sender: Any) { @@ -199,66 +204,92 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { } private func verifyKey() { - self.updateViewState(.verifyingKey) + guard let tunnelInfo = TunnelManager.shared.tunnelInfo else { return } - TunnelManager.shared.verifyPublicKey { (result) in - DispatchQueue.main.async { - switch result { - case .success(let isValid): - self.updateViewState(.verifiedKey(isValid)) - - case .failure(let error): - let alertController = UIAlertController( - title: NSLocalizedString("VERIFY_KEY_FAILURE_ALERT_TITLE", tableName: "WireguardKeys", comment: ""), - message: error.errorChainDescription, - preferredStyle: .alert - ) - alertController.addAction( - UIAlertAction(title: NSLocalizedString("VERIFY_KEY_FAILURE_ALERT_OK_ACTION", tableName: "WireguardKeys", comment: ""), style: .cancel) - ) + self.updateViewState(.verifyingKey) - self.alertPresenter.enqueue(alertController, presentingController: self) - self.updateViewState(.default) + REST.Client.shared.getWireguardKey(token: tunnelInfo.token, publicKey: tunnelInfo.tunnelSettings.interface.publicKey) + .map { _ in + return true + } + .flatMapError { error -> Result<Bool, REST.Error> in + if case .server(.pubKeyNotFound) = error { + return .success(false) + } else { + return .failure(error) } } - } + .receive(on: .main) + .storeCancellationToken(in: &verifyKeyCancellationToken) + .onSuccess { [weak self] isValid in + self?.updateViewState(.verifiedKey(isValid)) + } + .onFailure { [weak self] error in + self?.showKeyVerificationFailureAlert(error) + self?.updateViewState(.default) + } + .observe { _ in } } private func regeneratePrivateKey() { self.updateViewState(.regeneratingKey) - TunnelManager.shared.regeneratePrivateKey { [weak self] (result) in - DispatchQueue.main.async { - guard let self = self else { return } + TunnelManager.shared.regeneratePrivateKey() + .receive(on: .main) + .onSuccess { [weak self] _ in + self?.updateViewState(.regeneratedKey(true)) + } + .onFailure { [weak self] error in + self?.logger.error(chainedError: error, message: "Failed to regenerate the private key") - switch result { - case .success: - self.updateViewState(.regeneratedKey(true)) + self?.showKeyRegenerationFailureAlert(error) + self?.updateViewState(.regeneratedKey(false)) + } + .observe { _ in } + } - case .failure(let error): - let alertController = UIAlertController( - title: NSLocalizedString("REGENERATE_KEY_FAILURE_ALERT_TITLE", tableName: "WireguardKeys", comment: ""), - message: error.errorChainDescription, - preferredStyle: .alert - ) - alertController.addAction( - UIAlertAction(title: NSLocalizedString("REGENERATE_KEY_FAILURE_ALERT_OK_ACTION", tableName: "WireguardKeys", comment: ""), style: .cancel) - ) + private func showKeyVerificationFailureAlert(_ error: REST.Error) { + let reason = error.errorChainDescription ?? "" + let errorDescription = String( + format: NSLocalizedString( + "VERIFY_KEY_FAILURE_ALERT_MESSAGE", + tableName: "WireguardKeys", + value: "Failed to verify the WireGuard key on server: %@", + comment: "" + ), + reason + ) - self.logger.error(chainedError: error, message: "Failed to regenerate the private key") + let alertController = UIAlertController( + title: NSLocalizedString("VERIFY_KEY_FAILURE_ALERT_TITLE", tableName: "WireguardKeys", comment: ""), + message: errorDescription, + preferredStyle: .alert + ) - self.alertPresenter.enqueue(alertController, presentingController: self) + alertController.addAction( + UIAlertAction(title: NSLocalizedString("VERIFY_KEY_FAILURE_ALERT_OK_ACTION", tableName: "WireguardKeys", comment: ""), style: .cancel) + ) - self.updateViewState(.regeneratedKey(false)) - } - } - } + alertPresenter.enqueue(alertController, presentingController: self) + } + + private func showKeyRegenerationFailureAlert(_ error: TunnelManager.Error) { + let alertController = UIAlertController( + title: NSLocalizedString("REGENERATE_KEY_FAILURE_ALERT_TITLE", tableName: "WireguardKeys", comment: ""), + message: error.errorChainDescription, + preferredStyle: .alert + ) + alertController.addAction( + UIAlertAction(title: NSLocalizedString("REGENERATE_KEY_FAILURE_ALERT_OK_ACTION", tableName: "WireguardKeys", comment: ""), style: .cancel) + ) + + alertPresenter.enqueue(alertController, presentingController: self) } + private func setPublicKeyTitle(string: String, animated: Bool) { let updateTitle = { self.contentView.publicKeyRowView.value = string - } if animated { diff --git a/ios/MullvadVPN/en.lproj/Main.strings b/ios/MullvadVPN/en.lproj/Main.strings index 8e5b567739..8876a288f7 100644 --- a/ios/MullvadVPN/en.lproj/Main.strings +++ b/ios/MullvadVPN/en.lproj/Main.strings @@ -35,7 +35,16 @@ "TUNNEL_STATE_DISCONNECTED_ACCESSIBILITY_LABEL" = "Unsecured connection"; /* No comment provided by engineer. */ +"TUNNEL_STATE_DISCONNECTING" = "Disconnecting"; + +/* No comment provided by engineer. */ "TUNNEL_STATE_DISCONNECTING_ACCESSIBILITY_LABEL" = "Disconnecting"; /* No comment provided by engineer. */ +"TUNNEL_STATE_PENDING_RECONNECT" = "Reconnecting"; + +/* No comment provided by engineer. */ +"TUNNEL_STATE_PENDING_RECONNECT_ACCESSIBILITY_LABEL" = "Pending reconnect"; + +/* No comment provided by engineer. */ "TUNNEL_STATE_RECONNECTING_ACCESSIBILITY_LABEL" = "Reconnecting to %1$@, %2$@"; |
