summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj285
-rw-r--r--ios/MullvadVPN/Account.swift197
-rw-r--r--ios/MullvadVPN/AppDelegate.swift158
-rw-r--r--ios/MullvadVPN/ConnectMainContentView.swift14
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift204
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift31
-rw-r--r--ios/MullvadVPN/IPAddressRange+Codable.swift2
-rw-r--r--ios/MullvadVPN/Location.swift2
-rw-r--r--ios/MullvadVPN/NEVPNStatus+Debug.swift20
-rw-r--r--ios/MullvadVPN/PreferencesViewController.swift24
-rw-r--r--ios/MullvadVPN/PrivateKeyWithMetadata.swift8
-rw-r--r--ios/MullvadVPN/Promise/Promise+Delay.swift37
-rw-r--r--ios/MullvadVPN/REST/RESTClient.swift3
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProviderHost.swift3
-rw-r--r--ios/MullvadVPN/TunnelManager.swift1196
-rw-r--r--ios/MullvadVPN/TunnelManager/AnyTunnelObserver.swift36
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelInfo.swift18
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift1092
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManagerError.swift115
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelObserver.swift15
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState.swift70
-rw-r--r--ios/MullvadVPN/TunnelSettings.swift9
-rw-r--r--ios/MullvadVPN/WireguardAssociatedAddresses.swift2
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift145
-rw-r--r--ios/MullvadVPN/en.lproj/Main.strings9
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: &copyToPasteboardCancellationToken)
+ .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$@";