diff options
40 files changed, 1868 insertions, 1172 deletions
diff --git a/ios/.swiftformat b/ios/.swiftformat index ed479f35bd..2953ce46f8 100644 --- a/ios/.swiftformat +++ b/ios/.swiftformat @@ -11,4 +11,5 @@ --wrapcollections before-first --wrapternary before-operators --redundanttype inferred +--ifdef no-indent --disable initCoderUnavailable, redundantReturn, unusedArguments, redundantRawValues, preferKeyPath diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index ab0a074415..695bf14a17 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -33,6 +33,8 @@ Line wrap the file at 100 chars. Th - Add intents: start VPN, stop VPN, reconnect VPN (acts as start VPN when the tunnel is down, otherwise picks new relay). - Add menu item to control shortcuts. +- Add continuous monitoring of tunnel connection. Verify ping replies to detect whether traffic is + really flowing. ### Changed - When logged into an account with no time left, a new view is shown instead of account settings, diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index eb9a155cbe..dfd38e44e3 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -62,6 +62,8 @@ 58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB025124117005D0BB5 /* CustomTextField.swift */; }; 58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB2251241B3005D0BB5 /* CustomTextView.swift */; }; 58293FB725138B88005D0BB5 /* CustomNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB625138B88005D0BB5 /* CustomNavigationController.swift */; }; + 582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */; }; + 582A8A3B28BCE1AB00D0F9FB /* FixedWidthInteger+Arithmetics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58900D0228BBDCC70094E4F0 /* FixedWidthInteger+Arithmetics.swift */; }; 582AD44027BE616E002A6BFC /* CodingErrors+ChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AD43F27BE616E002A6BFC /* CodingErrors+ChainedError.swift */; }; 582AD44127BE6178002A6BFC /* CodingErrors+ChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AD43F27BE616E002A6BFC /* CodingErrors+ChainedError.swift */; }; 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */; }; @@ -127,7 +129,7 @@ 5857F24724C882D700CF6F47 /* SelectLocationNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5857F24624C882D700CF6F47 /* SelectLocationNavigationController.swift */; }; 585834F824D2BC1F00A8AF56 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 585834F724D2BC1F00A8AF56 /* Logging */; }; 585834FC24D2BC9500A8AF56 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 585834FB24D2BC9500A8AF56 /* Logging */; }; - 585B4B8726D9098900555C4C /* TunnelErrorNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE326CFD945001CB97C /* TunnelErrorNotificationProvider.swift */; }; + 585B4B8726D9098900555C4C /* TunnelStatusNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */; }; 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 */; }; @@ -144,8 +146,6 @@ 585DA8A626B14F5100B8C587 /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; }; 585E820327F3285E00939F0E /* SendAppStoreReceiptOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E820227F3285E00939F0E /* SendAppStoreReceiptOperation.swift */; }; 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; }; - 58655DCE27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */; }; - 58655DCF27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */; }; 5868585524054096000B8131 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868585424054096000B8131 /* AppButton.swift */; }; 5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */; }; 586E54FB27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */; }; @@ -192,6 +192,7 @@ 588BCF26280FE79A009ADCEC /* RESTProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588BCF25280FE79A009ADCEC /* RESTProxy.swift */; }; 588BCF282816D664009ADCEC /* RESTResponseHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588BCF272816D664009ADCEC /* RESTResponseHandler.swift */; }; 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; }; + 58900D0328BBDCC70094E4F0 /* FixedWidthInteger+Arithmetics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58900D0228BBDCC70094E4F0 /* FixedWidthInteger+Arithmetics.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 */; }; @@ -209,6 +210,7 @@ 589D288028462CB000F9A7B3 /* BackgroundObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D287F28462CB000F9A7B3 /* BackgroundObserver.swift */; }; 589D28822846306C00F9A7B3 /* GroupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28812846306C00F9A7B3 /* GroupOperation.swift */; }; 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; }; + 58A3BDB028A1821A00C8C2C6 /* WgStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3BDAF28A1821A00C8C2C6 /* WgStats.swift */; }; 58A8055E2716EA6700681642 /* AnyIPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26BE270C550B004EA533 /* AnyIPAddress.swift */; }; 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; 58A8D1D72892D3D60065405D /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; }; @@ -399,6 +401,9 @@ 5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+UIBackgroundFetchResult.swift"; sourceTree = "<group>"; }; 5820EDA8288FE064006BF4E4 /* DeviceManagementInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagementInteractor.swift; sourceTree = "<group>"; }; 5820EDAA288FF0D2006BF4E4 /* DeviceRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRowView.swift; sourceTree = "<group>"; }; + 58218E1428B65058000C624F /* IPv4Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IPv4Header.h; sourceTree = "<group>"; }; + 58218E1528B650C1000C624F /* ObjCBridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ObjCBridgingHeader.h; sourceTree = "<group>"; }; + 58218E1628B65396000C624F /* ICMPHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ICMPHeader.h; 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>"; }; 58289081286B590900478596 /* UIFont+Monospaced.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Monospaced.swift"; sourceTree = "<group>"; }; @@ -406,6 +411,7 @@ 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>"; }; 58293FB625138B88005D0BB5 /* CustomNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationController.swift; sourceTree = "<group>"; }; + 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixedWidthIntegerArithmeticsTests.swift; sourceTree = "<group>"; }; 582AD43F27BE616E002A6BFC /* CodingErrors+ChainedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodingErrors+ChainedError.swift"; sourceTree = "<group>"; }; 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountTokenInput.swift; sourceTree = "<group>"; }; 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTokenInputTests.swift; sourceTree = "<group>"; }; @@ -457,7 +463,6 @@ 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelStatus.swift; sourceTree = "<group>"; }; 585E820227F3285E00939F0E /* SendAppStoreReceiptOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendAppStoreReceiptOperation.swift; sourceTree = "<group>"; }; 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = "<group>"; }; - 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorConfiguration.swift; sourceTree = "<group>"; }; 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>"; }; @@ -498,6 +503,7 @@ 588BCF23280FE43D009ADCEC /* RESTDevicesProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTDevicesProxy.swift; sourceTree = "<group>"; }; 588BCF25280FE79A009ADCEC /* RESTProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTProxy.swift; sourceTree = "<group>"; }; 588BCF272816D664009ADCEC /* RESTResponseHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTResponseHandler.swift; sourceTree = "<group>"; }; + 58900D0228BBDCC70094E4F0 /* FixedWidthInteger+Arithmetics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+Arithmetics.swift"; sourceTree = "<group>"; }; 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEProviderStopReason+Debug.swift"; sourceTree = "<group>"; }; 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectSplitButton.swift; sourceTree = "<group>"; }; 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+ProductVersion.swift"; sourceTree = "<group>"; }; @@ -515,7 +521,8 @@ 589D28812846306C00F9A7B3 /* GroupOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupOperation.swift; sourceTree = "<group>"; }; 58A1AA8623F43901009F7EA6 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = "<group>"; }; 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionPanelView.swift; sourceTree = "<group>"; }; - 58A94AE326CFD945001CB97C /* TunnelErrorNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrorNotificationProvider.swift; sourceTree = "<group>"; }; + 58A3BDAF28A1821A00C8C2C6 /* WgStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgStats.swift; sourceTree = "<group>"; }; + 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusNotificationProvider.swift; sourceTree = "<group>"; }; 58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceViewController.swift; sourceTree = "<group>"; }; 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = "<group>"; }; 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSwitchCell.swift; sourceTree = "<group>"; }; @@ -812,7 +819,7 @@ isa = PBXGroup; children = ( 587B75402668FD7700DEF7E9 /* AccountExpiryNotificationProvider.swift */, - 58A94AE326CFD945001CB97C /* TunnelErrorNotificationProvider.swift */, + 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */, ); path = Notifications; sourceTree = "<group>"; @@ -830,6 +837,7 @@ 584B26F3237434D00073B10E /* RelaySelectorTests.swift */, 5807E2C1243203D000F5FF30 /* StringTests.swift */, 5819C2132726CC8D00D6EC38 /* DataSourceSnapshotTests.swift */, + 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */, ); path = MullvadVPNTests; sourceTree = "<group>"; @@ -1003,10 +1011,12 @@ isa = PBXGroup; children = ( 58CE5E7D224146470008646E /* Info.plist */, - 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */, + 58218E1528B650C1000C624F /* ObjCBridgingHeader.h */, 58CE5E7E224146470008646E /* PacketTunnel.entitlements */, 58E0729C28814AAE008902F8 /* PacketTunnelConfiguration.swift */, 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */, + 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */, + 58900D0228BBDCC70094E4F0 /* FixedWidthInteger+Arithmetics.swift */, 58E072A228814B96008902F8 /* TunnelMonitor */, 58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */, 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */, @@ -1029,8 +1039,10 @@ children = ( 5838318A27C40A3900000571 /* Pinger.swift */, 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */, - 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */, 58E072A428814C28008902F8 /* TunnelMonitorDelegate.swift */, + 58A3BDAF28A1821A00C8C2C6 /* WgStats.swift */, + 58218E1428B65058000C624F /* IPv4Header.h */, + 58218E1628B65396000C624F /* ICMPHeader.h */, ); path = TunnelMonitor; sourceTree = "<group>"; @@ -1288,6 +1300,7 @@ 584E96BE240FD4DB00D3334F /* Location.swift in Sources */, 5857F23424C8443700CF6F47 /* AsyncOperation.swift in Sources */, 585DA8A626B14F5100B8C587 /* SSLPinningURLSessionDelegate.swift in Sources */, + 582A8A3B28BCE1AB00D0F9FB /* FixedWidthInteger+Arithmetics.swift in Sources */, 58B0A2AC238EE6D500BC001D /* IPAddress+Codable.swift in Sources */, 58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */, 58DF5B7F2852778600E92647 /* OperationSmokeTests.swift in Sources */, @@ -1299,6 +1312,7 @@ 5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */, 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */, 58DF5B782852178600E92647 /* OperationInputInjectionTests.swift in Sources */, + 582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */, 58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */, 583E1E282848DE1C004838B3 /* BackgroundObserver.swift in Sources */, 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */, @@ -1385,7 +1399,6 @@ 5820EDA9288FE064006BF4E4 /* DeviceManagementInteractor.swift in Sources */, 58FB865A26EA214400F188BC /* RelayCacheObserver.swift in Sources */, 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */, - 58655DCE27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */, 5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */, 58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */, 589D287A2846250500F9A7B3 /* OperationCondition.swift in Sources */, @@ -1440,7 +1453,7 @@ 585E820327F3285E00939F0E /* SendAppStoreReceiptOperation.swift in Sources */, 584B17AB27637DE40057F3B8 /* ReconnectTunnelOperation.swift in Sources */, 5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */, - 585B4B8726D9098900555C4C /* TunnelErrorNotificationProvider.swift in Sources */, + 585B4B8726D9098900555C4C /* TunnelStatusNotificationProvider.swift in Sources */, 58FEAFB92750DA2F003C1625 /* AddressCache.swift in Sources */, 58B67B482602079E008EF58E /* RelaySelector.swift in Sources */, 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */, @@ -1566,7 +1579,6 @@ 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */, 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */, 580F8B872819795C002E0998 /* DNSSettings.swift in Sources */, - 58655DCF27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */, 5815039E24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */, 585DA87826B024A900B8C587 /* CachedRelays.swift in Sources */, 58E072A128814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift in Sources */, @@ -1574,7 +1586,9 @@ 58F840B32464491D0044E708 /* ChainedError.swift in Sources */, 58D67A0A26D7AE3300557C3C /* OSLogHandler.swift in Sources */, 5820675626E6528A00655B05 /* RESTError.swift in Sources */, + 58900D0328BBDCC70094E4F0 /* FixedWidthInteger+Arithmetics.swift in Sources */, 58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */, + 58A3BDB028A1821A00C8C2C6 /* WgStats.swift in Sources */, 58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */, 581503A024D6F01E00C9C50E /* LogRotation.swift in Sources */, 58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */, @@ -1790,14 +1804,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = MullvadVPN/MullvadVPN.entitlements; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; ENABLE_BITCODE = NO; INFOPLIST_FILE = MullvadVPN/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2022.2; + MARKETING_VERSION = 2022.3; PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -1814,14 +1828,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = MullvadVPN/MullvadVPN.entitlements; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; ENABLE_BITCODE = NO; INFOPLIST_FILE = MullvadVPN/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2022.2; + MARKETING_VERSION = 2022.3; PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -1835,7 +1849,7 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; ENABLE_BITCODE = NO; INFOPLIST_FILE = PacketTunnel/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1843,9 +1857,10 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2022.2; + MARKETING_VERSION = 2022.3; PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).PacketTunnel"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/PacketTunnel/ObjcBridgingHeader.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; @@ -1857,7 +1872,7 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; ENABLE_BITCODE = NO; INFOPLIST_FILE = PacketTunnel/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1865,9 +1880,10 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2022.2; + MARKETING_VERSION = 2022.3; PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).PacketTunnel"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/PacketTunnel/ObjcBridgingHeader.h"; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift index 82426b5623..5719328c27 100644 --- a/ios/MullvadVPN/AccountViewController.swift +++ b/ios/MullvadVPN/AccountViewController.swift @@ -317,7 +317,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { // no-op } diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 39a669b3fd..74670008a7 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -18,7 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private var logger: Logger! #if targetEnvironment(simulator) - private let simulatorTunnelProvider = SimulatorTunnelProviderHost() + private let simulatorTunnelProvider = SimulatorTunnelProviderHost() #endif private let operationQueue: AsyncOperationQueue = { @@ -41,8 +41,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { logger = Logger(label: "AppDelegate") #if targetEnvironment(simulator) - // Configure mock tunnel provider on simulator - SimulatorTunnelProvider.shared.delegate = simulatorTunnelProvider + // Configure mock tunnel provider on simulator + SimulatorTunnelProvider.shared.delegate = simulatorTunnelProvider #endif if #available(iOS 13.0, *) { @@ -379,7 +379,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private func setupNotificationHandler() { NotificationManager.shared.notificationProviders = [ AccountExpiryNotificationProvider(), - TunnelErrorNotificationProvider(), + TunnelStatusNotificationProvider(), ] UNUserNotificationCenter.current().delegate = self } diff --git a/ios/MullvadVPN/Bundle+ProductVersion.swift b/ios/MullvadVPN/Bundle+ProductVersion.swift index 1030096834..a3631c1872 100644 --- a/ios/MullvadVPN/Bundle+ProductVersion.swift +++ b/ios/MullvadVPN/Bundle+ProductVersion.swift @@ -22,13 +22,13 @@ extension Bundle { "???" #if DEBUG - return "\(version)-dev\(buildNumber)" + return "\(version)-dev\(buildNumber)" #else - if appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { - return "\(version)-beta\(buildNumber)" - } else { - return version - } + if appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { + return "\(version)-beta\(buildNumber)" + } else { + return version + } #endif } } diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift index 02e4a5ee6e..ebfd55f27b 100644 --- a/ios/MullvadVPN/ConnectViewController.swift +++ b/ios/MullvadVPN/ConnectViewController.swift @@ -183,8 +183,8 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen setNeedsHeaderBarStyleAppearanceUpdate() } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { - self.tunnelState = tunnelState + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { + tunnelState = tunnelStatus.state } func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { @@ -256,7 +256,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen case let .connected(tunnelRelay), let .reconnecting(tunnelRelay): setTunnelRelay(tunnelRelay) - case .disconnected, .disconnecting, .pendingReconnect: + case .disconnected, .disconnecting, .pendingReconnect, .waitingForConnectivity: setTunnelRelay(nil) } @@ -354,6 +354,9 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen removeLocationMarker() contentView.activityIndicator.startAnimating() + case .waitingForConnectivity: + removeLocationMarker() + case .disconnected, .disconnecting: removeLocationMarker() contentView.activityIndicator.stopAnimating() @@ -541,7 +544,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen private extension TunnelState { var textColorForSecureLabel: UIColor { switch self { - case .connecting, .reconnecting: + case .connecting, .reconnecting, .waitingForConnectivity: return .white case .connected: @@ -592,6 +595,14 @@ private extension TunnelState { value: "Unsecured connection", comment: "" ) + + case .waitingForConnectivity: + return NSLocalizedString( + "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY", + tableName: "Main", + value: "Blocked connection", + comment: "" + ) } } @@ -612,7 +623,7 @@ private extension TunnelState { value: "Select location", comment: "" ) - case .connecting, .connected, .reconnecting: + case .connecting, .connected, .reconnecting, .waitingForConnectivity: return NSLocalizedString( "SWITCH_LOCATION_BUTTON_TITLE", tableName: "Main", @@ -664,6 +675,14 @@ private extension TunnelState { tunnelInfo.location.country ) + case .waitingForConnectivity: + return NSLocalizedString( + "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Blocked connection", + comment: "" + ) + case .disconnecting(.nothing): return NSLocalizedString( "TUNNEL_STATE_DISCONNECTING_ACCESSIBILITY_LABEL", @@ -689,7 +708,8 @@ private extension TunnelState { case .disconnected, .disconnecting(.nothing): return [.selectLocation, .connect] - case .connecting, .pendingReconnect, .disconnecting(.reconnect): + case .connecting, .pendingReconnect, .disconnecting(.reconnect), + .waitingForConnectivity: return [.selectLocation, .cancel] case .connected, .reconnecting: @@ -701,7 +721,8 @@ private extension TunnelState { case .disconnected, .disconnecting(.nothing): return [.connect] - case .connecting, .pendingReconnect, .disconnecting(.reconnect): + case .connecting, .pendingReconnect, .disconnecting(.reconnect), + .waitingForConnectivity: return [.cancel] case .connected, .reconnecting: diff --git a/ios/MullvadVPN/Logging/Logging.swift b/ios/MullvadVPN/Logging/Logging.swift index 8d4f4f883f..a0c535853e 100644 --- a/ios/MullvadVPN/Logging/Logging.swift +++ b/ios/MullvadVPN/Logging/Logging.swift @@ -50,7 +50,7 @@ func initLoggingSystem(bundleIdentifier: String, metadata: Logger.Metadata? = ni var logHandlers: [LogHandler] = [] #if DEBUG - logHandlers.append(OSLogHandler(subsystem: bundleIdentifier, category: label)) + logHandlers.append(OSLogHandler(subsystem: bundleIdentifier, category: label)) #endif if !streams.isEmpty { diff --git a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift index 1ace9cc5bb..83cc888d82 100644 --- a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift @@ -154,7 +154,7 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio invalidate(deviceState: manager.deviceState) } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { // no-op } diff --git a/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift b/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift deleted file mode 100644 index 6c50218e22..0000000000 --- a/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// TunnelErrorNotificationProvider.swift -// TunnelErrorNotificationProvider -// -// Created by pronebird on 20/08/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -class TunnelErrorNotificationProvider: NotificationProvider, InAppNotificationProvider, - TunnelObserver -{ - override var identifier: String { - return "net.mullvad.MullvadVPN.TunnelErrorNotificationProvider" - } - - var notificationDescriptor: InAppNotificationDescriptor? { - guard let lastError = lastError else { return nil } - - let body = (lastError as? LocalizedNotificationError)?.localizedNotificationBody - ?? lastError.localizedDescription - - return InAppNotificationDescriptor( - identifier: identifier, - style: .error, - title: NSLocalizedString( - "TUNNEL_ERROR_INAPP_NOTIFICATION_TITLE", - value: "TUNNEL ERROR", - comment: "" - ), - body: body - ) - } - - private var lastError: Error? - - override init() { - super.init() - - TunnelManager.shared.addObserver(self) - } - - // MARK: - TunnelObserver - - func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) { - // no-op - } - - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { - // Reset error with each new connection attempt - if case .connecting = tunnelState { - lastError = nil - } - - // Tell manager to refresh displayed notifications - invalidate() - } - - func tunnelManager( - _ manager: TunnelManager, - didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2 - ) { - // no-op - } - - func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { - // no-op - } - - func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { - // Save tunnel error - lastError = error - - // Tell manager to refresh displayed notifications - invalidate() - } -} - -protocol LocalizedNotificationError { - var localizedNotificationBody: String? { get } -} - -extension StartTunnelError: LocalizedNotificationError { - var localizedNotificationBody: String? { - return String( - format: NSLocalizedString( - "START_TUNNEL_ERROR_INAPP_NOTIFICATION_BODY", - value: "Failed to start the tunnel: %@.", - comment: "" - ), - underlyingError.localizedDescription - ) - } -} - -extension StopTunnelError: LocalizedNotificationError { - var localizedNotificationBody: String? { - return String( - format: NSLocalizedString( - "STOP_TUNNEL_ERROR_INAPP_NOTIFICATION_BODY", - value: "Failed to stop the tunnel: %@.", - comment: "" - ), - underlyingError.localizedDescription - ) - } -} diff --git a/ios/MullvadVPN/Notifications/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/TunnelStatusNotificationProvider.swift new file mode 100644 index 0000000000..d19335972d --- /dev/null +++ b/ios/MullvadVPN/Notifications/TunnelStatusNotificationProvider.swift @@ -0,0 +1,198 @@ +// +// TunnelStatusNotificationProvider.swift +// TunnelStatusNotificationProvider +// +// Created by pronebird on 20/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +class TunnelStatusNotificationProvider: NotificationProvider, InAppNotificationProvider, + TunnelObserver +{ + private var isWaitingForConnectivity = false + private var packetTunnelError: String? + private var tunnelManagerError: Error? + + override var identifier: String { + return "net.mullvad.MullvadVPN.TunnelStatusNotificationProvider" + } + + var notificationDescriptor: InAppNotificationDescriptor? { + if let packetTunnelError = packetTunnelError { + return notificationDescription(for: packetTunnelError) + } else if let tunnelManagerError = tunnelManagerError { + return notificationDescription(for: tunnelManagerError) + } else if isWaitingForConnectivity { + return connectivityNotificationDescription() + } else { + return nil + } + } + + override init() { + super.init() + + let tunnelManager = TunnelManager.shared + tunnelManager.addObserver(self) + handleTunnelStatus(tunnelManager.tunnelStatus) + } + + // MARK: - Private + + private func handleTunnelStatus(_ tunnelStatus: TunnelStatus) { + let invalidateForTunnelError = updateLastTunnelError( + tunnelStatus.packetTunnelStatus + .lastError + ) + let invalidateForManagerError = updateTunnelManagerError(tunnelStatus.state) + let invalidateForConnectivity = updateConnectivity(tunnelStatus.state) + + if invalidateForTunnelError || invalidateForManagerError || invalidateForConnectivity { + invalidate() + } + } + + private func updateLastTunnelError(_ lastTunnelError: String?) -> Bool { + if packetTunnelError != lastTunnelError { + packetTunnelError = lastTunnelError + + return true + } + + return false + } + + private func updateConnectivity(_ tunnelState: TunnelState) -> Bool { + let isWaitingState = tunnelState == .waitingForConnectivity + + if isWaitingForConnectivity != isWaitingState { + isWaitingForConnectivity = isWaitingState + return true + } + + return false + } + + private func updateTunnelManagerError(_ tunnelState: TunnelState) -> Bool { + switch tunnelState { + case .connecting, .connected, .reconnecting: + // As of now, tunnel manager error can be received only when starting or stopping + // the tunnel. Make sure to reset it on each connection attempt. + if tunnelManagerError != nil { + tunnelManagerError = nil + return true + } + + default: + break + } + + return false + } + + private func notificationDescription(for packetTunnelError: String) + -> InAppNotificationDescriptor + { + return InAppNotificationDescriptor( + identifier: identifier, + style: .error, + title: NSLocalizedString( + "TUNNEL_LEAKING_INAPP_NOTIFICATION_TITLE", + value: "NETWORK TRAFFIC MIGHT BE LEAKING", + comment: "" + ), + body: String( + format: NSLocalizedString( + "PACKET_TUNNEL_ERROR_INAPP_NOTIFICATION_BODY", + value: "Could not configure VPN: %@", + comment: "" + ), + packetTunnelError + ) + ) + } + + private func notificationDescription(for error: Error) -> InAppNotificationDescriptor { + let body: String + + if let startError = error as? StartTunnelError { + body = String( + format: NSLocalizedString( + "START_TUNNEL_ERROR_INAPP_NOTIFICATION_BODY", + value: "Failed to start the tunnel: %@.", + comment: "" + ), + startError.underlyingError.localizedDescription + ) + } else if let stopError = error as? StopTunnelError { + body = String( + format: NSLocalizedString( + "STOP_TUNNEL_ERROR_INAPP_NOTIFICATION_BODY", + value: "Failed to stop the tunnel: %@.", + comment: "" + ), + stopError.underlyingError.localizedDescription + ) + } else { + body = error.localizedDescription + } + + return InAppNotificationDescriptor( + identifier: identifier, + style: .error, + title: NSLocalizedString( + "TUNNEL_MANAGER_ERROR_INAPP_NOTIFICATION_TITLE", + value: "TUNNEL ERROR", + comment: "" + ), + body: body + ) + } + + private func connectivityNotificationDescription() -> InAppNotificationDescriptor { + return InAppNotificationDescriptor( + identifier: identifier, + style: .warning, + title: NSLocalizedString( + "TUNNEL_NO_CONNECTIVITY_INAPP_NOTIFICATION_TITLE", + value: "NETWORK ISSUES", + comment: "" + ), + body: NSLocalizedString( + "TUNNEL_NO_CONNECTIVITY_INAPP_NOTIFICATION_BODY", + value: """ + Your device is offline. The tunnel will automatically connect once \ + your device is back online. + """, + comment: "" + ) + ) + } + + // MARK: - TunnelObserver + + func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) { + // no-op + } + + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { + handleTunnelStatus(tunnelStatus) + } + + func tunnelManager( + _ manager: TunnelManager, + didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2 + ) { + // no-op + } + + func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { + // no-op + } + + func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { + tunnelManagerError = error + } +} diff --git a/ios/MullvadVPN/Operations/BackgroundObserver.swift b/ios/MullvadVPN/Operations/BackgroundObserver.swift index b946d03440..ba924140f2 100644 --- a/ios/MullvadVPN/Operations/BackgroundObserver.swift +++ b/ios/MullvadVPN/Operations/BackgroundObserver.swift @@ -7,47 +7,47 @@ #if canImport(UIKit) - import UIKit +import UIKit - class BackgroundObserver: OperationObserver { - let name: String - let application: UIApplication - let cancelUponExpiration: Bool +class BackgroundObserver: OperationObserver { + let name: String + let application: UIApplication + let cancelUponExpiration: Bool - private var taskIdentifier: UIBackgroundTaskIdentifier? + private var taskIdentifier: UIBackgroundTaskIdentifier? - init( - application: UIApplication = .shared, - name: String, - cancelUponExpiration: Bool - ) { - self.application = application - self.name = name - self.cancelUponExpiration = cancelUponExpiration - } + init( + application: UIApplication = .shared, + name: String, + cancelUponExpiration: Bool + ) { + self.application = application + self.name = name + self.cancelUponExpiration = cancelUponExpiration + } - func didAttach(to operation: Operation) { - let expirationHandler = cancelUponExpiration ? { operation.cancel() } : nil + func didAttach(to operation: Operation) { + let expirationHandler = cancelUponExpiration ? { operation.cancel() } : nil - taskIdentifier = application.beginBackgroundTask( - withName: name, - expirationHandler: expirationHandler - ) - } + taskIdentifier = application.beginBackgroundTask( + withName: name, + expirationHandler: expirationHandler + ) + } - func operationDidStart(_ operation: Operation) { - // no-op - } + func operationDidStart(_ operation: Operation) { + // no-op + } - func operationDidCancel(_ operation: Operation) { - // no-op - } + func operationDidCancel(_ operation: Operation) { + // no-op + } - func operationDidFinish(_ operation: Operation, error: Error?) { - if let taskIdentifier = taskIdentifier { - application.endBackgroundTask(taskIdentifier) - } + func operationDidFinish(_ operation: Operation, error: Error?) { + if let taskIdentifier = taskIdentifier { + application.endBackgroundTask(taskIdentifier) } } +} #endif diff --git a/ios/MullvadVPN/OutOfTimeViewController.swift b/ios/MullvadVPN/OutOfTimeViewController.swift index 5b0c7fe89c..b37a1b0eb5 100644 --- a/ios/MullvadVPN/OutOfTimeViewController.swift +++ b/ios/MullvadVPN/OutOfTimeViewController.swift @@ -344,8 +344,8 @@ extension OutOfTimeViewController: AppStorePaymentObserver { extension OutOfTimeViewController: TunnelObserver { func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {} - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { - self.tunnelState = tunnelState + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { + tunnelState = tunnelStatus.state } func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) {} diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/PreferencesViewController.swift index 30edd298ba..152545b20d 100644 --- a/ios/MullvadVPN/PreferencesViewController.swift +++ b/ios/MullvadVPN/PreferencesViewController.swift @@ -81,7 +81,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { // no-op } diff --git a/ios/MullvadVPN/RevokedDeviceViewController.swift b/ios/MullvadVPN/RevokedDeviceViewController.swift index f4c02367ca..cb7ff2f7e1 100644 --- a/ios/MullvadVPN/RevokedDeviceViewController.swift +++ b/ios/MullvadVPN/RevokedDeviceViewController.swift @@ -170,9 +170,9 @@ class RevokedDeviceViewController: UIViewController, RootContainment, TunnelObse // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { setNeedsHeaderBarStyleAppearanceUpdate() - updateView(tunnelState: tunnelState) + updateView(tunnelState: tunnelStatus.state) } func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index 246e348c48..fc6951e0d5 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -271,7 +271,7 @@ extension SceneDelegate: RootContainerViewControllerDelegate { guard TunnelManager.shared.deviceState.isLoggedIn else { return false } switch TunnelManager.shared.tunnelStatus.state { - case .connected, .connecting, .reconnecting: + case .connected, .connecting, .reconnecting, .waitingForConnectivity: TunnelManager.shared.reconnectTunnel(selectNewRelay: true) case .disconnecting, .disconnected: TunnelManager.shared.startTunnel() @@ -932,7 +932,7 @@ extension SceneDelegate: TunnelObserver { configureScene() } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { // no-op } diff --git a/ios/MullvadVPN/SettingsDataSource.swift b/ios/MullvadVPN/SettingsDataSource.swift index 44e4891570..0f0f9b6cd2 100644 --- a/ios/MullvadVPN/SettingsDataSource.swift +++ b/ios/MullvadVPN/SettingsDataSource.swift @@ -269,7 +269,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { // no-op } diff --git a/ios/MullvadVPN/SimulatorTunnelProvider.swift b/ios/MullvadVPN/SimulatorTunnelProvider.swift index 162a95590f..4c7e0e091a 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider.swift @@ -48,388 +48,388 @@ extension NETunnelProviderManager: VPNTunnelProviderManagerProtocol {} #if targetEnvironment(simulator) - // MARK: - NEPacketTunnelProvider stubs +// MARK: - NEPacketTunnelProvider stubs - class SimulatorTunnelProviderDelegate { - fileprivate(set) var connection: SimulatorVPNConnection? +class SimulatorTunnelProviderDelegate { + fileprivate(set) var connection: SimulatorVPNConnection? - var protocolConfiguration: NEVPNProtocol { - return connection?.protocolConfiguration ?? NEVPNProtocol() - } - - var reasserting: Bool { - get { - return connection?.reasserting ?? false - } - set { - connection?.reasserting = newValue - } - } + var protocolConfiguration: NEVPNProtocol { + return connection?.protocolConfiguration ?? NEVPNProtocol() + } - func startTunnel( - options: [String: NSObject]?, - completionHandler: @escaping (Error?) -> Void - ) { - completionHandler(nil) + var reasserting: Bool { + get { + return connection?.reasserting ?? false } - - func stopTunnel( - with reason: NEProviderStopReason, - completionHandler: @escaping () -> Void - ) { - completionHandler() + set { + connection?.reasserting = newValue } + } - func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - completionHandler?(nil) - } + func startTunnel( + options: [String: NSObject]?, + completionHandler: @escaping (Error?) -> Void + ) { + completionHandler(nil) } - class SimulatorTunnelProvider { - static let shared = SimulatorTunnelProvider() + func stopTunnel( + with reason: NEProviderStopReason, + completionHandler: @escaping () -> Void + ) { + completionHandler() + } - private let lock = NSLock() - private var _delegate: SimulatorTunnelProviderDelegate? + func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { + completionHandler?(nil) + } +} - var delegate: SimulatorTunnelProviderDelegate! { - get { - lock.lock() - defer { lock.unlock() } +class SimulatorTunnelProvider { + static let shared = SimulatorTunnelProvider() - return _delegate - } - set { - lock.lock() - _delegate = newValue - lock.unlock() - } - } + private let lock = NSLock() + private var _delegate: SimulatorTunnelProviderDelegate? - private init() {} + var delegate: SimulatorTunnelProviderDelegate! { + get { + lock.lock() + defer { lock.unlock() } - fileprivate func handleAppMessage( - _ messageData: Data, - completionHandler: ((Data?) -> Void)? = nil - ) { - delegate.handleAppMessage(messageData, completionHandler: completionHandler) + return _delegate + } + set { + lock.lock() + _delegate = newValue + lock.unlock() } } - // MARK: - NEVPNConnection stubs + private init() {} - class SimulatorVPNConnection: NSObject, VPNConnectionProtocol { - // Protocol configuration is automatically synced by `SimulatorTunnelInfo` - fileprivate var protocolConfiguration = NEVPNProtocol() - - private let lock = NSRecursiveLock() - private var _status: NEVPNStatus = .disconnected - private var _reasserting = false - private var _connectedDate: Date? + fileprivate func handleAppMessage( + _ messageData: Data, + completionHandler: ((Data?) -> Void)? = nil + ) { + delegate.handleAppMessage(messageData, completionHandler: completionHandler) + } +} - private(set) var status: NEVPNStatus { - get { - lock.lock() - defer { lock.unlock() } +// MARK: - NEVPNConnection stubs - return _status - } - set { - lock.lock() +class SimulatorVPNConnection: NSObject, VPNConnectionProtocol { + // Protocol configuration is automatically synced by `SimulatorTunnelInfo` + fileprivate var protocolConfiguration = NEVPNProtocol() - if _status != newValue { - _status = newValue + private let lock = NSRecursiveLock() + private var _status: NEVPNStatus = .disconnected + private var _reasserting = false + private var _connectedDate: Date? - // Send notification while holding the lock. This should enable the receiver - // to fetch the `SimulatorVPNConnection.status` before the concurrent code gets - // opportunity to change it again. - postStatusDidChangeNotification() - } + private(set) var status: NEVPNStatus { + get { + lock.lock() + defer { lock.unlock() } - lock.unlock() - } + return _status } + set { + lock.lock() - var reasserting: Bool { - get { - lock.lock() - defer { lock.unlock() } + if _status != newValue { + _status = newValue - return _reasserting + // Send notification while holding the lock. This should enable the receiver + // to fetch the `SimulatorVPNConnection.status` before the concurrent code gets + // opportunity to change it again. + postStatusDidChangeNotification() } - set { - lock.lock() - if _reasserting != newValue { - _reasserting = newValue + lock.unlock() + } + } - if newValue { - status = .reasserting - } else { - status = .connected - } - } + var reasserting: Bool { + get { + lock.lock() + defer { lock.unlock() } - lock.unlock() - } + return _reasserting } + set { + lock.lock() - private(set) var connectedDate: Date? { - get { - lock.lock() - defer { lock.unlock() } + if _reasserting != newValue { + _reasserting = newValue - return _connectedDate - } - set { - lock.lock() - _connectedDate = newValue - lock.unlock() + if newValue { + status = .reasserting + } else { + status = .connected + } } - } - func startVPNTunnel() throws { - try startVPNTunnel(options: nil) + lock.unlock() } + } - func startVPNTunnel(options: [String: NSObject]?) throws { - SimulatorTunnelProvider.shared.delegate.connection = self - - status = .connecting + private(set) var connectedDate: Date? { + get { + lock.lock() + defer { lock.unlock() } - SimulatorTunnelProvider.shared.delegate.startTunnel(options: options) { error in - if error == nil { - self.status = .connected - self.connectedDate = Date() - } else { - self.status = .disconnected - self.connectedDate = nil - } - } + return _connectedDate + } + set { + lock.lock() + _connectedDate = newValue + lock.unlock() } + } - func stopVPNTunnel() { - status = .disconnecting + func startVPNTunnel() throws { + try startVPNTunnel(options: nil) + } + + func startVPNTunnel(options: [String: NSObject]?) throws { + SimulatorTunnelProvider.shared.delegate.connection = self - SimulatorTunnelProvider.shared.delegate.stopTunnel(with: .userInitiated) { + status = .connecting + + SimulatorTunnelProvider.shared.delegate.startTunnel(options: options) { error in + if error == nil { + self.status = .connected + self.connectedDate = Date() + } else { self.status = .disconnected self.connectedDate = nil } } + } + + func stopVPNTunnel() { + status = .disconnecting - private func postStatusDidChangeNotification() { - NotificationCenter.default.post(name: .NEVPNStatusDidChange, object: self) + SimulatorTunnelProvider.shared.delegate.stopTunnel(with: .userInitiated) { + self.status = .disconnected + self.connectedDate = nil } } - // MARK: - NETunnelProviderSession stubs + private func postStatusDidChangeNotification() { + NotificationCenter.default.post(name: .NEVPNStatusDidChange, object: self) + } +} - class SimulatorTunnelProviderSession: SimulatorVPNConnection, VPNTunnelProviderSessionProtocol { - func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws { - SimulatorTunnelProvider.shared.handleAppMessage( - messageData, - completionHandler: responseHandler - ) - } +// MARK: - NETunnelProviderSession stubs + +class SimulatorTunnelProviderSession: SimulatorVPNConnection, VPNTunnelProviderSessionProtocol { + func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws { + SimulatorTunnelProvider.shared.handleAppMessage( + messageData, + completionHandler: responseHandler + ) } +} - // MARK: - NETunnelProviderManager stubs +// MARK: - NETunnelProviderManager stubs - /// A mock struct for tunnel configuration and connection - private struct SimulatorTunnelInfo { - /// A unique identifier for the configuration - var identifier = UUID().uuidString +/// A mock struct for tunnel configuration and connection +private struct SimulatorTunnelInfo { + /// A unique identifier for the configuration + var identifier = UUID().uuidString - /// An associated VPN connection. - /// Intentionally initialized with a `SimulatorTunnelProviderSession` subclass which - /// implements the necessary protocol - var connection: SimulatorVPNConnection = SimulatorTunnelProviderSession() + /// An associated VPN connection. + /// Intentionally initialized with a `SimulatorTunnelProviderSession` subclass which + /// implements the necessary protocol + var connection: SimulatorVPNConnection = SimulatorTunnelProviderSession() - /// Whether configuration is enabled - var isEnabled = false + /// Whether configuration is enabled + var isEnabled = false - /// Whether on-demand VPN is enabled - var isOnDemandEnabled = false + /// Whether on-demand VPN is enabled + var isOnDemandEnabled = false - /// On-demand VPN rules - var onDemandRules = [NEOnDemandRule]() + /// On-demand VPN rules + var onDemandRules = [NEOnDemandRule]() - /// Protocol configuration - var protocolConfiguration: NEVPNProtocol? { - didSet { - connection.protocolConfiguration = protocolConfiguration ?? NEVPNProtocol() - } + /// Protocol configuration + var protocolConfiguration: NEVPNProtocol? { + didSet { + connection.protocolConfiguration = protocolConfiguration ?? NEVPNProtocol() } + } - /// Tunnel description - var localizedDescription: String? + /// Tunnel description + var localizedDescription: String? - /// Designated initializer - init() {} - } + /// Designated initializer + init() {} +} - class SimulatorTunnelProviderManager: VPNTunnelProviderManagerProtocol, Equatable { - static let tunnelsLock = NSRecursiveLock() - fileprivate static var tunnels = [SimulatorTunnelInfo]() +class SimulatorTunnelProviderManager: VPNTunnelProviderManagerProtocol, Equatable { + static let tunnelsLock = NSRecursiveLock() + fileprivate static var tunnels = [SimulatorTunnelInfo]() - private let lock = NSLock() - private var tunnelInfo: SimulatorTunnelInfo - private var identifier: String { + private let lock = NSLock() + private var tunnelInfo: SimulatorTunnelInfo + private var identifier: String { + lock.lock() + defer { lock.unlock() } + + return tunnelInfo.identifier + } + + var isOnDemandEnabled: Bool { + get { lock.lock() defer { lock.unlock() } - return tunnelInfo.identifier + return tunnelInfo.isOnDemandEnabled } - - var isOnDemandEnabled: Bool { - get { - lock.lock() - defer { lock.unlock() } - - return tunnelInfo.isOnDemandEnabled - } - set { - lock.lock() - tunnelInfo.isOnDemandEnabled = newValue - lock.unlock() - } + set { + lock.lock() + tunnelInfo.isOnDemandEnabled = newValue + lock.unlock() } + } - var onDemandRules: [NEOnDemandRule] { - get { - lock.lock() - defer { lock.unlock() } + var onDemandRules: [NEOnDemandRule] { + get { + lock.lock() + defer { lock.unlock() } - return tunnelInfo.onDemandRules - } - set { - lock.lock() - tunnelInfo.onDemandRules = newValue - lock.unlock() - } + return tunnelInfo.onDemandRules } - - var isEnabled: Bool { - get { - lock.lock() - defer { lock.unlock() } - - return tunnelInfo.isEnabled - } - set { - lock.lock() - tunnelInfo.isEnabled = newValue - lock.unlock() - } + set { + lock.lock() + tunnelInfo.onDemandRules = newValue + lock.unlock() } + } - var protocolConfiguration: NEVPNProtocol? { - get { - lock.lock() - defer { lock.unlock() } + var isEnabled: Bool { + get { + lock.lock() + defer { lock.unlock() } - return tunnelInfo.protocolConfiguration - } - set { - lock.lock() - tunnelInfo.protocolConfiguration = newValue - lock.unlock() - } + return tunnelInfo.isEnabled } + set { + lock.lock() + tunnelInfo.isEnabled = newValue + lock.unlock() + } + } - var localizedDescription: String? { - get { - lock.lock() - defer { lock.unlock() } + var protocolConfiguration: NEVPNProtocol? { + get { + lock.lock() + defer { lock.unlock() } - return tunnelInfo.localizedDescription - } - set { - lock.lock() - tunnelInfo.localizedDescription = newValue - lock.unlock() - } + return tunnelInfo.protocolConfiguration } + set { + lock.lock() + tunnelInfo.protocolConfiguration = newValue + lock.unlock() + } + } - var connection: SimulatorVPNConnection { + var localizedDescription: String? { + get { lock.lock() defer { lock.unlock() } - return tunnelInfo.connection + return tunnelInfo.localizedDescription } + set { + lock.lock() + tunnelInfo.localizedDescription = newValue + lock.unlock() + } + } - static func loadAllFromPreferences(completionHandler: ( - [SimulatorTunnelProviderManager]?, - Error? - ) -> Void) { - Self.tunnelsLock.lock() - let tunnelProviders = tunnels.map { tunnelInfo in - return SimulatorTunnelProviderManager(tunnelInfo: tunnelInfo) - } - Self.tunnelsLock.unlock() + var connection: SimulatorVPNConnection { + lock.lock() + defer { lock.unlock() } - completionHandler(tunnelProviders, nil) - } + return tunnelInfo.connection + } - required convenience init() { - self.init(tunnelInfo: SimulatorTunnelInfo()) + static func loadAllFromPreferences(completionHandler: ( + [SimulatorTunnelProviderManager]?, + Error? + ) -> Void) { + Self.tunnelsLock.lock() + let tunnelProviders = tunnels.map { tunnelInfo in + return SimulatorTunnelProviderManager(tunnelInfo: tunnelInfo) } + Self.tunnelsLock.unlock() - private init(tunnelInfo: SimulatorTunnelInfo) { - self.tunnelInfo = tunnelInfo - } + completionHandler(tunnelProviders, nil) + } - func loadFromPreferences(completionHandler: (Error?) -> Void) { - var error: NEVPNError? + required convenience init() { + self.init(tunnelInfo: SimulatorTunnelInfo()) + } - Self.tunnelsLock.lock() + private init(tunnelInfo: SimulatorTunnelInfo) { + self.tunnelInfo = tunnelInfo + } - if let savedTunnel = Self.tunnels.first(where: { $0.identifier == self.identifier }) { - tunnelInfo = savedTunnel - } else { - error = NEVPNError(.configurationInvalid) - } + func loadFromPreferences(completionHandler: (Error?) -> Void) { + var error: NEVPNError? - Self.tunnelsLock.unlock() + Self.tunnelsLock.lock() - completionHandler(error) + if let savedTunnel = Self.tunnels.first(where: { $0.identifier == self.identifier }) { + tunnelInfo = savedTunnel + } else { + error = NEVPNError(.configurationInvalid) } - func saveToPreferences(completionHandler: ((Error?) -> Void)?) { - Self.tunnelsLock.lock() + Self.tunnelsLock.unlock() - if let index = Self.tunnels.firstIndex(where: { $0.identifier == self.identifier }) { - Self.tunnels[index] = tunnelInfo - } else { - Self.tunnels.append(tunnelInfo) - } + completionHandler(error) + } - Self.tunnelsLock.unlock() + func saveToPreferences(completionHandler: ((Error?) -> Void)?) { + Self.tunnelsLock.lock() - completionHandler?(nil) + if let index = Self.tunnels.firstIndex(where: { $0.identifier == self.identifier }) { + Self.tunnels[index] = tunnelInfo + } else { + Self.tunnels.append(tunnelInfo) } - func removeFromPreferences(completionHandler: ((Error?) -> Void)?) { - var error: NEVPNError? + Self.tunnelsLock.unlock() - Self.tunnelsLock.lock() + completionHandler?(nil) + } - if let index = Self.tunnels.firstIndex(where: { $0.identifier == self.identifier }) { - Self.tunnels.remove(at: index) - } else { - error = NEVPNError(.configurationReadWriteFailed) - } + func removeFromPreferences(completionHandler: ((Error?) -> Void)?) { + var error: NEVPNError? - Self.tunnelsLock.unlock() + Self.tunnelsLock.lock() - completionHandler?(error) + if let index = Self.tunnels.firstIndex(where: { $0.identifier == self.identifier }) { + Self.tunnels.remove(at: index) + } else { + error = NEVPNError(.configurationReadWriteFailed) } - static func == ( - lhs: SimulatorTunnelProviderManager, - rhs: SimulatorTunnelProviderManager - ) -> Bool { - lhs.identifier == rhs.identifier - } + Self.tunnelsLock.unlock() + + completionHandler?(error) } + static func == ( + lhs: SimulatorTunnelProviderManager, + rhs: SimulatorTunnelProviderManager + ) -> Bool { + lhs.identifier == rhs.identifier + } +} + #endif diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift index 9c3b2623a7..31d28ad84c 100644 --- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift @@ -8,109 +8,109 @@ #if targetEnvironment(simulator) - import Foundation - import Logging - import enum NetworkExtension.NEProviderStopReason +import Foundation +import Logging +import enum NetworkExtension.NEProviderStopReason - class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { - private var selectorResult: RelaySelectorResult? +class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { + private var selectorResult: RelaySelectorResult? - private let providerLogger = Logger(label: "SimulatorTunnelProviderHost") - private let dispatchQueue = DispatchQueue(label: "SimulatorTunnelProviderHostQueue") + private let providerLogger = Logger(label: "SimulatorTunnelProviderHost") + private let dispatchQueue = DispatchQueue(label: "SimulatorTunnelProviderHostQueue") - override func startTunnel( - options: [String: NSObject]?, - completionHandler: @escaping (Error?) -> Void - ) { - dispatchQueue.async { - var selectorResult: RelaySelectorResult? + override func startTunnel( + options: [String: NSObject]?, + completionHandler: @escaping (Error?) -> Void + ) { + dispatchQueue.async { + var selectorResult: RelaySelectorResult? - do { - let tunnelOptions = PacketTunnelOptions(rawOptions: options ?? [:]) + do { + let tunnelOptions = PacketTunnelOptions(rawOptions: options ?? [:]) - selectorResult = try tunnelOptions.getSelectorResult() - } catch { - self.providerLogger.error( - chainedError: AnyChainedError(error), - message: """ - Failed to decode relay selector result passed from the app. \ - Will continue by picking new relay. - """ - ) - } + selectorResult = try tunnelOptions.getSelectorResult() + } catch { + self.providerLogger.error( + chainedError: AnyChainedError(error), + message: """ + Failed to decode relay selector result passed from the app. \ + Will continue by picking new relay. + """ + ) + } - do { - self.selectorResult = try selectorResult ?? self.pickRelay() + do { + self.selectorResult = try selectorResult ?? self.pickRelay() - completionHandler(nil) - } catch { - self.providerLogger.error( - chainedError: AnyChainedError(error), - message: "Failed to pick relay." - ) - completionHandler(error) - } + completionHandler(nil) + } catch { + self.providerLogger.error( + chainedError: AnyChainedError(error), + message: "Failed to pick relay." + ) + completionHandler(error) } } + } - override func stopTunnel( - with reason: NEProviderStopReason, - completionHandler: @escaping () -> Void - ) { - dispatchQueue.async { - self.selectorResult = nil + override func stopTunnel( + with reason: NEProviderStopReason, + completionHandler: @escaping () -> Void + ) { + dispatchQueue.async { + self.selectorResult = nil - completionHandler() - } + completionHandler() } + } - override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - dispatchQueue.async { - do { - let response = try self.processMessage(messageData) + override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { + dispatchQueue.async { + do { + let response = try self.processMessage(messageData) - completionHandler?(response) - } catch { - self.providerLogger.error( - chainedError: AnyChainedError(error), - message: "Failed to handle app message." - ) + completionHandler?(response) + } catch { + self.providerLogger.error( + chainedError: AnyChainedError(error), + message: "Failed to handle app message." + ) - completionHandler?(nil) - } + completionHandler?(nil) } } + } - private func processMessage(_ messageData: Data) throws -> Data? { - let message = try TunnelProviderMessage(messageData: messageData) - - switch message { - case .getTunnelStatus: - var tunnelStatus = PacketTunnelStatus() - tunnelStatus.tunnelRelay = self.selectorResult?.packetTunnelRelay + private func processMessage(_ messageData: Data) throws -> Data? { + let message = try TunnelProviderMessage(messageData: messageData) - return try TunnelProviderReply(tunnelStatus).encode() + switch message { + case .getTunnelStatus: + var tunnelStatus = PacketTunnelStatus() + tunnelStatus.tunnelRelay = self.selectorResult?.packetTunnelRelay - case let .reconnectTunnel(aSelectorResult): - reasserting = true - if let aSelectorResult = aSelectorResult { - selectorResult = aSelectorResult - } - reasserting = false + return try TunnelProviderReply(tunnelStatus).encode() - return nil + case let .reconnectTunnel(aSelectorResult): + reasserting = true + if let aSelectorResult = aSelectorResult { + selectorResult = aSelectorResult } + reasserting = false + + return nil } + } - private func pickRelay() throws -> RelaySelectorResult { - let cachedRelays = try RelayCache.Tracker.shared.getCachedRelays() - let tunnelSettings = try SettingsManager.readSettings() + private func pickRelay() throws -> RelaySelectorResult { + let cachedRelays = try RelayCache.Tracker.shared.getCachedRelays() + let tunnelSettings = try SettingsManager.readSettings() - return try RelaySelector.evaluate( - relays: cachedRelays.relays, - constraints: tunnelSettings.relayConstraints - ) - } + return try RelaySelector.evaluate( + relays: cachedRelays.relays, + constraints: tunnelSettings.relayConstraints + ) } +} #endif diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift index c34e2fc8c0..4adc15e0ef 100644 --- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift @@ -39,26 +39,41 @@ class MapConnectionStatusOperation: AsyncOperation { switch connectionStatus { case .connecting: switch tunnelState { - case .connecting(.some(_)): + case .connecting: break + default: - interactor.updateTunnelState(.connecting(nil)) + interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus.state = .connecting(nil) + } } - updateTunnelRelayAndFinish(tunnel: tunnel) { relay in - return relay.map { .connecting($0) } + fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in + if packetTunnelStatus.isNetworkReachable { + return packetTunnelStatus.tunnelRelay.map { .connecting($0) } + } else { + return .waitingForConnectivity + } } return case .reasserting: - updateTunnelRelayAndFinish(tunnel: tunnel) { relay in - return relay.map { .reconnecting($0) } + fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in + if packetTunnelStatus.isNetworkReachable { + return packetTunnelStatus.tunnelRelay.map { .reconnecting($0) } + } else { + return .waitingForConnectivity + } } return case .connected: - updateTunnelRelayAndFinish(tunnel: tunnel) { relay in - return relay.map { .connected($0) } + fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in + if packetTunnelStatus.isNetworkReachable { + return packetTunnelStatus.tunnelRelay.map { .connected($0) } + } else { + return .waitingForConnectivity + } } return @@ -69,12 +84,17 @@ class MapConnectionStatusOperation: AsyncOperation { case .disconnecting(.reconnect): logger.debug("Restart the tunnel on disconnect.") - - interactor.resetTunnelState(to: .pendingReconnect) + interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus = TunnelStatus() + tunnelStatus.state = .pendingReconnect + } interactor.startTunnel() default: - interactor.resetTunnelState(to: .disconnected) + interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus = TunnelStatus() + tunnelStatus.state = .disconnected + } } case .disconnecting: @@ -82,11 +102,17 @@ class MapConnectionStatusOperation: AsyncOperation { case .disconnecting: break default: - interactor.resetTunnelState(to: .disconnecting(.nothing)) + interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus = TunnelStatus() + tunnelStatus.state = .disconnecting(.nothing) + } } case .invalid: - interactor.resetTunnelState(to: .disconnected) + interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus = TunnelStatus() + tunnelStatus.state = .disconnected + } @unknown default: logger.debug("Unknown NEVPNStatus: \(connectionStatus.rawValue)") @@ -99,19 +125,22 @@ class MapConnectionStatusOperation: AsyncOperation { request?.cancel() } - private func updateTunnelRelayAndFinish( + private func fetchTunnelStatus( tunnel: Tunnel, - mapRelayToState: @escaping (PacketTunnelRelay?) -> TunnelState? + mapToState: @escaping (PacketTunnelStatus) -> TunnelState? ) { request = tunnel.getTunnelStatus { [weak self] completion in guard let self = self else { return } self.dispatchQueue.async { if case let .success(packetTunnelStatus) = completion, !self.isCancelled { - self.interactor.updateTunnelStatus( - from: packetTunnelStatus, - mappingRelayToState: mapRelayToState - ) + self.interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus.packetTunnelStatus = packetTunnelStatus + + if let newState = mapToState(packetTunnelStatus) { + tunnelStatus.state = newState + } + } } self.finish() diff --git a/ios/MullvadVPN/TunnelManager/PacketTunnelStatus.swift b/ios/MullvadVPN/TunnelManager/PacketTunnelStatus.swift index 9283e37cb8..8a49815651 100644 --- a/ios/MullvadVPN/TunnelManager/PacketTunnelStatus.swift +++ b/ios/MullvadVPN/TunnelManager/PacketTunnelStatus.swift @@ -10,12 +10,12 @@ import Foundation /// Struct describing packet tunnel process status. struct PacketTunnelStatus: Codable, Equatable { + /// Last tunnel error. + var lastError: String? = nil + /// Flag indicating whether network is reachable. var isNetworkReachable = true - /// When the packet tunnel started connecting. - var connectingDate: Date? - /// Current relay. var tunnelRelay: PacketTunnelRelay? } diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index 12d7f9b3a5..812869caaa 100644 --- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift @@ -315,7 +315,10 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, Error> { self.interactor.prepareForVPNConfigurationDeletion() // Reset tunnel and device state. - self.interactor.resetTunnelState(to: .disconnected) + self.interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus = TunnelStatus() + tunnelStatus.state = .disconnected + } self.interactor.setDeviceState(.loggedOut, persist: true) // Finish immediately if tunnel provider is not set. diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index f30867bc97..f11164961e 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -38,7 +38,10 @@ class StartTunnelOperation: ResultOperation<Void, Error> { switch interactor.tunnelStatus.state { case .disconnecting(.nothing): - interactor.updateTunnelState(.disconnecting(.reconnect)) + interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus = TunnelStatus() + tunnelStatus.state = .disconnecting(.reconnect) + } finish(completion: .success(())) @@ -103,7 +106,12 @@ class StartTunnelOperation: ResultOperation<Void, Error> { Tunnel(tunnelProvider: tunnelProvider), shouldRefreshTunnelState: false ) - interactor.resetTunnelState(to: .connecting(selectorResult.packetTunnelRelay)) + + interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus = TunnelStatus() + tunnelStatus.packetTunnelStatus.tunnelRelay = selectorResult.packetTunnelRelay + tunnelStatus.state = .connecting(selectorResult.packetTunnelRelay) + } try tunnelProvider.connection.startVPNTunnel(options: tunnelOptions.rawOptions()) } diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift index 00814f024b..766626cea3 100644 --- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift @@ -28,11 +28,13 @@ class StopTunnelOperation: ResultOperation<Void, Error> { override func main() { switch interactor.tunnelStatus.state { case .disconnecting(.reconnect): - interactor.updateTunnelState(.disconnecting(.nothing)) + interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus.state = .disconnecting(.nothing) + } finish(completion: .success(())) - case .connected, .connecting, .reconnecting: + case .connected, .connecting, .reconnecting, .waitingForConnectivity: guard let tunnel = interactor.tunnel else { finish(completion: .failure(UnsetTunnelError())) return @@ -52,7 +54,7 @@ class StopTunnelOperation: ResultOperation<Void, Error> { } } - default: + case .disconnected, .disconnecting, .pendingReconnect: finish(completion: .success(())) } } diff --git a/ios/MullvadVPN/TunnelManager/Tunnel.swift b/ios/MullvadVPN/TunnelManager/Tunnel.swift index 682b969485..260037b7a4 100644 --- a/ios/MullvadVPN/TunnelManager/Tunnel.swift +++ b/ios/MullvadVPN/TunnelManager/Tunnel.swift @@ -11,9 +11,9 @@ import NetworkExtension // Switch to stabs on simulator #if targetEnvironment(simulator) - typealias TunnelProviderManagerType = SimulatorTunnelProviderManager +typealias TunnelProviderManagerType = SimulatorTunnelProviderManager #else - typealias TunnelProviderManagerType = NETunnelProviderManager +typealias TunnelProviderManagerType = NETunnelProviderManager #endif protocol TunnelStatusObserver { diff --git a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift index b2f19523b0..f4fb16079b 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift @@ -14,20 +14,11 @@ protocol TunnelInteractor { var tunnel: Tunnel? { get } func setTunnel(_ tunnel: Tunnel?, shouldRefreshTunnelState: Bool) - // MARK: - Tunnel status manipulation + // MARK: - Tunnel status var tunnelStatus: TunnelStatus { get } - - func setTunnelStatus(_ tunnelStatus: TunnelStatus) - func updateTunnelStatus( - from packetTunnelStatus: PacketTunnelStatus, - mappingRelayToState mapper: (PacketTunnelRelay?) -> TunnelState? - ) - - // MARK: - Tunnel state - - func updateTunnelState(_ state: TunnelState) - func resetTunnelState(to state: TunnelState) + @discardableResult func updateTunnelStatus(_ block: (inout TunnelStatus) -> Void) + -> TunnelStatus // MARK: - Configuration diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 2014a90cc0..00e203fb3a 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -13,28 +13,19 @@ import StoreKit import UIKit import class WireGuardKitTypes.PublicKey -enum TunnelManagerConfiguration { - /// Delay used before starting to quickly poll the tunnel (in seconds). - /// Usually when the tunnel is either starting or when reconnecting for a brief moment, until - /// the tunnel broadcasts the connecting date which is later used to synchronize polling. - static let tunnelStatusQuickPollDelay: TimeInterval = 1 +/// Interval used for periodic pollingg of tunnel relay status when tunnel is establishing +/// connection. +private let establishingTunnelStatusPollInterval: TimeInterval = 3 - /// Poll interval used when connecting date is unknown (in seconds). - static let tunnelStatusQuickPollInterval: TimeInterval = 3 +/// Interval used for periodic polling of tunnel connectivity status once the tunnel connection +/// is established. +private let establishedTunnelStatusPollInterval: TimeInterval = 5 - /// Delay used for when connecting date is known (in seconds). - /// Since both GUI and packet tunnel run timers, this accounts for some leeway. - static let tunnelStatusLongPollDelay: TimeInterval = 0.25 +/// Private key rotation interval (in seconds). +private let privateKeyRotationInterval: TimeInterval = 60 * 60 * 24 * 4 - /// Poll interval used for when connecting date is known (in seconds). - static let tunnelStatusLongPollInterval = TunnelMonitorConfiguration.connectionTimeout - - /// Private key rotation interval (in seconds). - static let privateKeyRotationInterval: TimeInterval = 60 * 60 * 24 * 4 - - /// Private key rotation retry interval (in seconds). - static let privateKeyRotationFailureRetryInterval: TimeInterval = 60 * 15 -} +/// Private key rotation retry interval (in seconds). +private let privateKeyRotationFailureRetryInterval: TimeInterval = 60 * 15 /// A class that provides a convenient interface for VPN tunnels configuration, manipulation and /// monitoring. @@ -78,7 +69,6 @@ final class TunnelManager { private var tunnelStatusPollTimer: DispatchSourceTimer? private var isPolling = false - private var lastConnectingDate: Date? private var _isConfigurationLoaded = false private var _deviceState: DeviceState = .loggedOut @@ -148,7 +138,7 @@ final class TunnelManager { // Retry at equal interval if failed or cancelled. if !completion.isSuccess { let date = lastAttemptDate.addingTimeInterval( - TunnelManagerConfiguration.privateKeyRotationFailureRetryInterval + privateKeyRotationFailureRetryInterval ) return max(date, Date()) @@ -156,8 +146,7 @@ final class TunnelManager { } // Rotate at long intervals otherwise. - let date = deviceData.wgKeyData.creationDate - .addingTimeInterval(TunnelManagerConfiguration.privateKeyRotationInterval) + let date = deviceData.wgKeyData.creationDate.addingTimeInterval(privateKeyRotationInterval) return max(date, Date()) } @@ -256,7 +245,9 @@ final class TunnelManager { } func refreshTunnelStatus() { + #if DEBUG logger.debug("Refresh tunnel status due to application becoming active.") + #endif _refreshTunnelStatus() } @@ -453,7 +444,7 @@ final class TunnelManager { ) -> Cancellable { var rotationInterval: TimeInterval? if !forceRotate { - rotationInterval = TunnelManagerConfiguration.privateKeyRotationInterval + rotationInterval = privateKeyRotationInterval } let operation = RotateKeyOperation( @@ -608,39 +599,16 @@ final class TunnelManager { } } - fileprivate func updateTunnelState(_ state: TunnelState) { - nslock.lock() - defer { nslock.unlock() } - - var updatedStatus = _tunnelStatus - updatedStatus.state = state - setTunnelStatus(updatedStatus) - } - - fileprivate func updateTunnelStatus( - from packetTunnelStatus: PacketTunnelStatus, - mappingRelayToState mapper: (PacketTunnelRelay?) -> TunnelState? - ) { - nslock.lock() - defer { nslock.unlock() } - - var updatedStatus = _tunnelStatus - updatedStatus.update(from: packetTunnelStatus, mappingRelayToState: mapper) - setTunnelStatus(updatedStatus) - } - - fileprivate func resetTunnelStatus(to state: TunnelState) { + fileprivate func setTunnelStatus(_ block: (inout TunnelStatus) -> Void) -> TunnelStatus { nslock.lock() defer { nslock.unlock() } - var updatedStatus = _tunnelStatus - updatedStatus.reset(to: state) - setTunnelStatus(updatedStatus) - } + var newTunnelStatus = _tunnelStatus + block(&newTunnelStatus) - fileprivate func setTunnelStatus(_ newTunnelStatus: TunnelStatus) { - nslock.lock() - defer { nslock.unlock() } + guard _tunnelStatus != newTunnelStatus else { + return newTunnelStatus + } logger.info("Status: \(newTunnelStatus).") @@ -650,20 +618,24 @@ final class TunnelManager { case .connecting, .reconnecting: // Start polling tunnel status to keep the relay information up to date // while the tunnel process is trying to connect. - startPollingTunnelStatus( - connectingDate: newTunnelStatus.packetTunnelStatus.connectingDate - ) + startPollingTunnelStatus(interval: establishingTunnelStatusPollInterval) + + case .connected, .waitingForConnectivity: + // Start polling tunnel status to keep connectivity status up to date. + startPollingTunnelStatus(interval: establishedTunnelStatusPollInterval) - case .pendingReconnect, .connected, .disconnecting, .disconnected: + case .pendingReconnect, .disconnecting, .disconnected: // Stop polling tunnel status once connection moved to final state. cancelPollingTunnelStatus() } DispatchQueue.main.async { self.observerList.forEach { observer in - observer.tunnelManager(self, didUpdateTunnelState: newTunnelStatus.state) + observer.tunnelManager(self, didUpdateTunnelStatus: newTunnelStatus) } } + + return newTunnelStatus } fileprivate func setSettings(_ settings: TunnelSettingsV2, persist: Bool) { @@ -896,69 +868,21 @@ final class TunnelManager { // MARK: - Tunnel status polling - private func computeNextPollDateAndRepeatInterval(connectingDate: Date?) - -> (Date, TimeInterval) - { - let delay, repeating: TimeInterval - let fireDate: Date - - if let connectingDate = connectingDate { - // Compute the schedule date for timer relative to when the packet tunnel started - // connecting. - delay = TunnelManagerConfiguration.tunnelStatusLongPollDelay - repeating = TunnelManagerConfiguration.tunnelStatusLongPollInterval - - // Compute the time elapsed since connecting date. - let elapsed = max(0, Date().timeIntervalSince(connectingDate)) - - // Compute how many times the timer has fired so far. - let fireCount = floor(elapsed / repeating) + private func startPollingTunnelStatus(interval: TimeInterval) { + guard !isPolling else { return } - // Compute when the timer will fire next time. - let nextDelta = (fireCount + 1) * repeating - - // Compute the fire date adding extra delay to account for leeway. - fireDate = connectingDate.addingTimeInterval(nextDelta + delay) - } else { - // Do quick polling until it's known when the packet tunnel started connecting. - delay = TunnelManagerConfiguration.tunnelStatusQuickPollDelay - repeating = TunnelManagerConfiguration.tunnelStatusQuickPollInterval - - fireDate = Date(timeIntervalSinceNow: delay) - } - - return (fireDate, repeating) - } - - private func startPollingTunnelStatus(connectingDate: Date?) { - guard lastConnectingDate != connectingDate || !isPolling else { return } - - lastConnectingDate = connectingDate isPolling = true - let ( - fireDate, - repeating - ) = computeNextPollDateAndRepeatInterval(connectingDate: connectingDate) - logger - .debug( - "Start polling tunnel status at \(fireDate.logFormatDate()) every \(repeating) second(s)." - ) + logger.debug( + "Start polling tunnel status every \(interval) second(s)." + ) let timer = DispatchSource.makeTimerSource(queue: .main) timer.setEventHandler { [weak self] in - guard let self = self else { return } - - self.logger.debug("Refresh tunnel status (poll).") - self._refreshTunnelStatus() + self?._refreshTunnelStatus() } - - timer.schedule( - wallDeadline: .now() + fireDate.timeIntervalSinceNow, - repeating: repeating - ) - - timer.resume() + timer.schedule(wallDeadline: .now() + interval, repeating: interval) + timer.activate() tunnelStatusPollTimer?.cancel() tunnelStatusPollTimer = timer @@ -971,7 +895,6 @@ final class TunnelManager { tunnelStatusPollTimer?.cancel() tunnelStatusPollTimer = nil - lastConnectingDate = nil isPolling = false } } @@ -1033,23 +956,8 @@ private struct TunnelInteractorProxy: TunnelInteractor { return tunnelManager.tunnelStatus } - func setTunnelStatus(_ tunnelStatus: TunnelStatus) { - tunnelManager.setTunnelStatus(tunnelStatus) - } - - func updateTunnelStatus( - from packetTunnelStatus: PacketTunnelStatus, - mappingRelayToState mapper: (PacketTunnelRelay?) -> TunnelState? - ) { - tunnelManager.updateTunnelStatus(from: packetTunnelStatus, mappingRelayToState: mapper) - } - - func updateTunnelState(_ state: TunnelState) { - tunnelManager.updateTunnelState(state) - } - - func resetTunnelState(to state: TunnelState) { - tunnelManager.resetTunnelStatus(to: state) + func updateTunnelStatus(_ block: (inout TunnelStatus) -> Void) -> TunnelStatus { + return tunnelManager.setTunnelStatus(block) } var isConfigurationLoaded: Bool { diff --git a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift index 7a3c4f910f..148da699ae 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift @@ -10,7 +10,7 @@ import Foundation protocol TunnelObserver: AnyObject { func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) func tunnelManager( diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift index fc06f1cd0d..fd58e48d51 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift @@ -10,7 +10,7 @@ import Foundation /// A struct describing the tunnel status. struct TunnelStatus: Equatable, CustomStringConvertible { - /// Tunnel status returned by the tunnel process. + /// Tunnel status returned by tunnel process. var packetTunnelStatus = PacketTunnelStatus() /// Tunnel state. @@ -25,32 +25,8 @@ struct TunnelStatus: Equatable, CustomStringConvertible { s += "unreachable" } - if let connectingDate = packetTunnelStatus.connectingDate { - s += ", started connecting at \(connectingDate.logFormatDate())" - } - return s } - - /// Updates the tunnel status from packet tunnel status, mapping relay to tunnel state. - mutating func update( - from packetTunnelStatus: PacketTunnelStatus, - mappingRelayToState mapper: (PacketTunnelRelay?) -> TunnelState? - ) { - self.packetTunnelStatus = packetTunnelStatus - - if let newState = mapper(packetTunnelStatus.tunnelRelay) { - state = newState - } - } - - /// Resets all fields to their defaults and assigns the next tunnel state. - mutating func reset(to newState: TunnelState) { - let currentRelay = packetTunnelStatus.tunnelRelay - packetTunnelStatus = PacketTunnelStatus() - packetTunnelStatus.tunnelRelay = currentRelay - state = newState - } } /// An enum that describes the tunnel state. @@ -59,7 +35,7 @@ enum TunnelState: Equatable, CustomStringConvertible { case pendingReconnect /// Connecting the tunnel. - case connecting(_ relay: PacketTunnelRelay?) + case connecting(PacketTunnelRelay?) /// Connected the tunnel case connected(PacketTunnelRelay) @@ -70,9 +46,15 @@ enum TunnelState: Equatable, CustomStringConvertible { /// 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(_ relay: PacketTunnelRelay) + /// Reconnecting the tunnel. + /// Transition to this state happens when: + /// 1. Asking the running tunnel to reconnect to new relay via IPC. + /// 2. Tunnel attempts to reconnect to new relay as the current relay appears to be + /// dysfunctional. + case reconnecting(PacketTunnelRelay) + + /// Waiting for connectivity to come back up. + case waitingForConnectivity var description: String { switch self { @@ -92,12 +74,14 @@ enum TunnelState: Equatable, CustomStringConvertible { return "disconnected" case let .reconnecting(tunnelRelay): return "reconnecting to \(tunnelRelay.hostname)" + case .waitingForConnectivity: + return "waiting for connectivity" } } var isSecured: Bool { switch self { - case .reconnecting, .connecting, .connected: + case .reconnecting, .connecting, .connected, .waitingForConnectivity: return true case .pendingReconnect, .disconnecting, .disconnected: return false @@ -105,12 +89,12 @@ enum TunnelState: Equatable, CustomStringConvertible { } } -/// A enum that describes the action to perform after disconnect -enum ActionAfterDisconnect { - /// Do nothing after disconnecting +/// A enum that describes the action to perform after disconnect. +enum ActionAfterDisconnect: CustomStringConvertible { + /// Do nothing after disconnecting. case nothing - /// Reconnect after disconnecting + /// Reconnect after disconnecting. case reconnect var description: String { diff --git a/ios/MullvadVPNTests/FixedWidthIntegerArithmeticsTests.swift b/ios/MullvadVPNTests/FixedWidthIntegerArithmeticsTests.swift new file mode 100644 index 0000000000..bfbf2a0cfa --- /dev/null +++ b/ios/MullvadVPNTests/FixedWidthIntegerArithmeticsTests.swift @@ -0,0 +1,36 @@ +// +// FixedWidthIntegerArithmeticsTests.swift +// MullvadVPNTests +// +// Created by pronebird on 29/08/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +class FixedWidthIntegerArithmeticsTests: XCTestCase { + func testSaturatingMultiplication() { + XCTAssertEqual(Int16.max.saturatingMultiplication(10), .max) + XCTAssertEqual(Int16.min.saturatingMultiplication(10), .min) + XCTAssertEqual(Int16.max.saturatingMultiplication(-10), .min) + XCTAssertEqual(Int16.min.saturatingMultiplication(-10), .max) + } + + func testSaturatingAddition() { + XCTAssertEqual(Int16.max.saturatingAddition(1), .max) + XCTAssertEqual(Int16.min.saturatingAddition(-1), .min) + } + + func testSaturatingSubtraction() { + XCTAssertEqual(Int16.min.saturatingSubtraction(100), .min) + XCTAssertEqual(Int16.max.saturatingSubtraction(-1), .max) + XCTAssertEqual(Int16.min.saturatingSubtraction(-1), .min + 1) + XCTAssertEqual(Int16.max.saturatingSubtraction(1), .max - 1) + } + + func testSaturatingPow() { + XCTAssertEqual(Int16(-4).saturatingPow(3), -64) + XCTAssertEqual(Int16.min.saturatingPow(2), .max) + XCTAssertEqual(Int16.min.saturatingPow(3), .min) + } +} diff --git a/ios/PacketTunnel/FixedWidthInteger+Arithmetics.swift b/ios/PacketTunnel/FixedWidthInteger+Arithmetics.swift new file mode 100644 index 0000000000..71d1ace4d2 --- /dev/null +++ b/ios/PacketTunnel/FixedWidthInteger+Arithmetics.swift @@ -0,0 +1,65 @@ +// +// FixedWidthInteger+Arithmetics.swift +// PacketTunnel +// +// Created by pronebird on 28/08/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension FixedWidthInteger { + /// Saturating integer multiplication. Computes `self * rhs`, saturating at the numeric bounds + /// instead of overflowing. + func saturatingMultiplication(_ rhs: Self) -> Self { + let (partialValue, isOverflow) = multipliedReportingOverflow(by: rhs) + + if isOverflow { + return signum() == rhs.signum() ? .max : .min + } else { + return partialValue + } + } + + /// Saturating integer addition. Computes `self + rhs`, saturating at the numeric bounds + /// instead of overflowing. + func saturatingAddition(_ rhs: Self) -> Self { + let (partialValue, isOverflow) = addingReportingOverflow(rhs) + + if isOverflow { + return partialValue.signum() >= 0 ? .min : .max + } else { + return partialValue + } + } + + /// Saturating integer subtraction. Computes `self - rhs`, saturating at the numeric bounds + /// instead of overflowing. + func saturatingSubtraction(_ rhs: Self) -> Self { + let (partialValue, isOverflow) = subtractingReportingOverflow(rhs) + + if isOverflow { + return partialValue.signum() >= 0 ? .min : .max + } else { + return partialValue + } + } + + /// Saturating integer exponentiation. Computes `self ** exp`, saturating at the numeric + /// bounds instead of overflowing. + func saturatingPow(_ exp: UInt32) -> Self { + let result = pow(Double(self), Double(exp)) + + if result.isFinite { + if result <= Double(Self.min) { + return .min + } else if result >= Double(Self.max) { + return .max + } else { + return Self(result) + } + } else { + return result.sign == .minus ? Self.min : Self.max + } + } +} diff --git a/ios/PacketTunnel/ObjCBridgingHeader.h b/ios/PacketTunnel/ObjCBridgingHeader.h new file mode 100644 index 0000000000..4e57f0de38 --- /dev/null +++ b/ios/PacketTunnel/ObjCBridgingHeader.h @@ -0,0 +1,15 @@ +// +// ObjCBridgingHeader.h +// MullvadVPN +// +// Created by pronebird on 24/08/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +#ifndef OBJCBRIDGINGHEADER_H +#define OBJCBRIDGINGHEADER_H + +#include "IPv4Header.h" +#include "ICMPHeader.h" + +#endif /* OBJCBRIDGINGHEADER_H */ diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 89de247198..c2b64fb07e 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -32,19 +32,19 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { /// Flag indicating whether network is reachable. private var isNetworkReachable = true - /// When the packet tunnel started connecting. - private var connectingDate: Date? + /// Last runtime error. + private var lastError: Error? /// Current selector result. private var selectorResult: RelaySelectorResult? /// A system completion handler passed from startTunnel and saved for later use once the /// connection is established. - private var startTunnelCompletionHandler: ((Error?) -> Void)? + private var startTunnelCompletionHandler: (() -> Void)? /// A completion handler passed during reassertion and saved for later use once the connection /// is reestablished. - private var reassertTunnelCompletionHandler: ((Error?) -> Void)? + private var reassertTunnelCompletionHandler: (() -> Void)? /// Tunnel monitor. private var tunnelMonitor: TunnelMonitor! @@ -52,8 +52,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { /// Returns `PacketTunnelStatus` used for sharing with main bundle process. private var packetTunnelStatus: PacketTunnelStatus { return PacketTunnelStatus( + lastError: lastError?.localizedDescription, isNetworkReachable: isNetworkReachable, - connectingDate: connectingDate, tunnelRelay: selectorResult?.packetTunnelRelay ) } @@ -123,7 +123,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { // Read tunnel configuration. let tunnelConfiguration: PacketTunnelConfiguration do { - tunnelConfiguration = try makeConfiguration(appSelectorResult) + let initialRelay: NextRelay = appSelectorResult.map { .set($0) } ?? .automatic + + tunnelConfiguration = try makeConfiguration(initialRelay) } catch { providerLogger.error( chainedError: AnyChainedError(error), @@ -154,20 +156,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { } else { self.providerLogger.debug("Started the tunnel.") - // Store completion handler and call it from TunnelMonitorDelegate once - // the connection is established. - self.startTunnelCompletionHandler = { [weak self] error in - // Mark the tunnel connected. + self.startTunnelCompletionHandler = { [weak self] in self?.isConnected = true - - // Call system completion handler. - completionHandler(error) + completionHandler(nil) } - // Start tunnel monitor. - let gatewayAddress = tunnelConfiguration.selectorResult.endpoint.ipv4Gateway - - self.startTunnelMonitor(gatewayAddress: gatewayAddress) + self.tunnelMonitor.start( + probeAddress: tunnelConfiguration.selectorResult.endpoint.ipv4Gateway + ) } } } @@ -180,11 +176,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { providerLogger.debug("Stop the tunnel: \(reason)") dispatchQueue.async { - // Stop tunnel monitor. self.tunnelMonitor.stop() - // Unset the start tunnel completion handler. self.startTunnelCompletionHandler = nil + self.reassertTunnelCompletionHandler = nil } adapter.stop { error in @@ -223,18 +218,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { case let .reconnectTunnel(appSelectorResult): self.providerLogger.debug("Reconnecting the tunnel...") - self.reconnectTunnel(selectorResult: appSelectorResult) { [weak self] error in - guard let self = self else { return } + let nextRelay: NextRelay = (appSelectorResult ?? self.selectorResult) + .map { .set($0) } ?? .automatic - if let error = error { - self.providerLogger.error( - chainedError: AnyChainedError(error), - message: "Failed to reconnect the tunnel." - ) - } else { - self.providerLogger.debug("Reconnected the tunnel.") - } - } + self.reconnectTunnel(to: nextRelay) completionHandler?(nil) @@ -263,92 +250,73 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { // Add code here to wake up. } - // MARK: - TunnelMonitor + // MARK: - TunnelMonitorDelegate func tunnelMonitorDidDetermineConnectionEstablished(_ tunnelMonitor: TunnelMonitor) { dispatchPrecondition(condition: .onQueue(dispatchQueue)) providerLogger.debug("Connection established.") - connectingDate = nil - - startTunnelCompletionHandler?(nil) + startTunnelCompletionHandler?() startTunnelCompletionHandler = nil - reassertTunnelCompletionHandler?(nil) + reassertTunnelCompletionHandler?() reassertTunnelCompletionHandler = nil + + setReconnecting(false) } - func tunnelMonitorDelegateShouldHandleConnectionRecovery(_ tunnelMonitor: TunnelMonitor) { + func tunnelMonitorDelegate( + _ tunnelMonitor: TunnelMonitor, + shouldHandleConnectionRecoveryWithCompletion completionHandler: @escaping () -> Void + ) { dispatchPrecondition(condition: .onQueue(dispatchQueue)) providerLogger.debug("Recover connection. Picking next relay...") - let handleRecoveryFailure = { (error: Error) in - // Stop tunnel monitor. - tunnelMonitor.stop() - - // Call start tunnel completion handler with error. - self.startTunnelCompletionHandler?(error) - - // Reset start tunnel completion handler. - self.startTunnelCompletionHandler = nil - - // Call tunnel reassertion completion handler with error. - self.reassertTunnelCompletionHandler?(error) - - // Reset tunnel reassertion completion handler. - self.reassertTunnelCompletionHandler = nil - } - - // Read tunnel configuration. - let tunnelConfiguration: PacketTunnelConfiguration - do { - tunnelConfiguration = try makeConfiguration(nil) - } catch { - handleRecoveryFailure(error) - return - } - - // Update tunnel status. - let tunnelRelay = tunnelConfiguration.selectorResult.packetTunnelRelay - selectorResult = tunnelConfiguration.selectorResult - providerLogger.debug("Set tunnel relay to \(tunnelRelay.hostname).") - - // Update WireGuard configuration. - adapter.update(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in - self.dispatchQueue.async { - if let error = error { - handleRecoveryFailure(error) - } - } - } + reconnectTunnel(to: .automatic, completionHandler: completionHandler) } func tunnelMonitor( _ tunnelMonitor: TunnelMonitor, networkReachabilityStatusDidChange isNetworkReachable: Bool ) { + guard self.isNetworkReachable != isNetworkReachable else { return } + self.isNetworkReachable = isNetworkReachable - // Adjust the start reconnect date if tunnel monitor re-started pinging in response to - // network connectivity coming back up. - if let startDate = tunnelMonitor.startDate { - connectingDate = startDate + // Switch tunnel into reconnecting state when offline. + if !isNetworkReachable { + setReconnecting(true) } } // MARK: - Private - private func makeConfiguration(_ appSelectorResult: RelaySelectorResult? = nil) + private func setReconnecting(_ reconnecting: Bool) { + // Raise reasserting flag, but only if tunnel has already moved to connected state once. + // Otherwise keep the app in connecting state until it manages to establish the very first + // connection. + if isConnected { + reasserting = reconnecting + } + } + + private func makeConfiguration(_ nextRelay: NextRelay) throws -> PacketTunnelConfiguration { let deviceState = try SettingsManager.readDeviceState() let tunnelSettings = try SettingsManager.readSettings() - let selectorResult = try appSelectorResult - ?? Self.selectRelayEndpoint( + let selectorResult: RelaySelectorResult + + switch nextRelay { + case .automatic: + selectorResult = try Self.selectRelayEndpoint( relayConstraints: tunnelSettings.relayConstraints ) + case let .set(aSelectorResult): + selectorResult = aSelectorResult + } return PacketTunnelConfiguration( deviceState: deviceState, @@ -357,18 +325,19 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { ) } - private func reconnectTunnel( - selectorResult aSelectorResult: RelaySelectorResult?, - completionHandler: @escaping (Error?) -> Void - ) { + private func reconnectTunnel(to nextRelay: NextRelay, completionHandler: (() -> Void)? = nil) { dispatchPrecondition(condition: .onQueue(dispatchQueue)) // Read tunnel configuration. let tunnelConfiguration: PacketTunnelConfiguration do { - tunnelConfiguration = try makeConfiguration(aSelectorResult ?? selectorResult) + tunnelConfiguration = try makeConfiguration(nextRelay) } catch { - completionHandler(error) + providerLogger.error( + chainedError: AnyChainedError(error), + message: "Failed produce new configuration." + ) + completionHandler?() return } @@ -378,68 +347,41 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { // Update tunnel status. selectorResult = tunnelConfiguration.selectorResult - connectingDate = nil providerLogger.debug("Set tunnel relay to \(newTunnelRelay.hostname).") + setReconnecting(true) - // Raise reasserting flag, but only if tunnel has already moved to connected state once. - // Otherwise keep the app in connecting state until it manages to establish the very first - // connection. - if isConnected { - reasserting = true - } - - // Update WireGuard configuration. adapter.update(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in self.dispatchQueue.async { - // Reset previously stored completion handler. - self.reassertTunnelCompletionHandler = nil + self.lastError = error - // Call completion handler immediately on error to update adapter configuration. if let error = error { - // Revert to previously used relay selector. + self.providerLogger.error( + chainedError: AnyChainedError(error), + message: "Failed to update WireGuard configuration." + ) + + // Revert to previously used relay selector as it's very likely that we keep + // using previous configuration. self.selectorResult = oldSelectorResult self.providerLogger.debug( "Reset tunnel relay to \(oldSelectorResult?.relay.hostname ?? "none")." ) + self.reassertTunnelCompletionHandler = nil + self.setReconnecting(false) - // Lower the reasserting flag. - if self.isConnected { - self.reasserting = false - } - - // Call completion handler immediately. - completionHandler(error) + completionHandler?() } else { - // Store completion handler and call it from TunnelMonitorDelegate once - // the connection is established. - self.reassertTunnelCompletionHandler = { [weak self] providerError in - guard let self = self else { return } - - // Lower the reasserting flag. - if self.isConnected { - self.reasserting = false - } - - completionHandler(providerError) - } - - // Restart tunnel monitor. - let gatewayAddress = tunnelConfiguration.selectorResult.endpoint.ipv4Gateway + self.reassertTunnelCompletionHandler = completionHandler - self.startTunnelMonitor(gatewayAddress: gatewayAddress) + self.tunnelMonitor.start( + probeAddress: tunnelConfiguration.selectorResult.endpoint.ipv4Gateway + ) } } } } - private func startTunnelMonitor(gatewayAddress: IPv4Address) { - tunnelMonitor.start(address: gatewayAddress) - - // Mark when the tunnel started monitoring connection. - connectingDate = tunnelMonitor.startDate - } - /// Load relay cache with potential networking to refresh the cache and pick the relay for the /// given relay constraints. private class func selectRelayEndpoint(relayConstraints: RelayConstraints) throws @@ -460,3 +402,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { ) } } + +/// Enum describing the next relay to connect to. +private enum NextRelay { + /// Connect to pre-selected relay. + case set(RelaySelectorResult) + + /// Determine next relay using relay selector. + case automatic +} diff --git a/ios/PacketTunnel/TunnelMonitor/ICMPHeader.h b/ios/PacketTunnel/TunnelMonitor/ICMPHeader.h new file mode 100644 index 0000000000..7e91f576d2 --- /dev/null +++ b/ios/PacketTunnel/TunnelMonitor/ICMPHeader.h @@ -0,0 +1,24 @@ +// +// ICMPHeader.h +// MullvadVPN +// +// Created by pronebird on 24/08/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +#ifndef ICMPHEADER_H +#define ICMPHEADER_H + +struct ICMPHeader { + uint8_t type; + uint8_t code; + uint16_t checksum; + uint16_t identifier; + uint16_t sequenceNumber; + // data... +} __attribute__((packed)); +typedef struct ICMPHeader ICMPHeader; + +__Check_Compile_Time(sizeof(ICMPHeader) == 8); + +#endif /* ICMPHEADER_H */ diff --git a/ios/PacketTunnel/TunnelMonitor/IPv4Header.h b/ios/PacketTunnel/TunnelMonitor/IPv4Header.h new file mode 100644 index 0000000000..bcb0b8ecc9 --- /dev/null +++ b/ios/PacketTunnel/TunnelMonitor/IPv4Header.h @@ -0,0 +1,33 @@ +// +// IPv4Header.h +// MullvadVPN +// +// Created by pronebird on 24/08/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +#ifndef IPV4HEADER_H +#define IPV4HEADER_H + +#include <stdint.h> +#include <AssertMacros.h> + +struct IPv4Header { + uint8_t versionAndHeaderLength; + uint8_t differentiatedServices; + uint16_t totalLength; + uint16_t identification; + uint16_t flagsAndFragmentOffset; + uint8_t timeToLive; + uint8_t protocol; + uint16_t headerChecksum; + uint8_t sourceAddress[4]; + uint8_t destinationAddress[4]; + // options... + // data... +} __attribute__((packed)); +typedef struct IPv4Header IPv4Header; + +__Check_Compile_Time(sizeof(IPv4Header) == 20); + +#endif /* IPV4HEADER_H */ diff --git a/ios/PacketTunnel/TunnelMonitor/Pinger.swift b/ios/PacketTunnel/TunnelMonitor/Pinger.swift index ca4071fef2..f063ac51e7 100644 --- a/ios/PacketTunnel/TunnelMonitor/Pinger.swift +++ b/ios/PacketTunnel/TunnelMonitor/Pinger.swift @@ -8,45 +8,96 @@ import Foundation import Logging +import protocol Network.IPAddress import struct Network.IPv4Address +import struct Network.IPv6Address + +protocol PingerDelegate: AnyObject { + func pinger( + _ pinger: Pinger, + didReceiveResponseFromSender senderAddress: IPAddress, + icmpHeader: ICMPHeader + ) + + func pinger( + _ pinger: Pinger, + didFailWithError error: Error + ) +} final class Pinger { + struct SendResult { + var sequenceNumber: UInt16 + var bytesSent: UInt16 + } + + // Socket read buffer size. + private static let bufferSize = 65535 + // Sender identifier passed along with ICMP packet. - private let identifier: UInt16 = 757 + private let identifier: UInt16 private var sequenceNumber: UInt16 = 0 private var socket: CFSocket? - - private let address: IPv4Address - private let interfaceName: String? + private var readBuffer = [UInt8](repeating: 0, count: bufferSize) private let logger = Logger(label: "Pinger") private let stateLock = NSRecursiveLock() - private var timer: DispatchSourceTimer? - init(address: IPv4Address, interfaceName: String?) { - self.address = address - self.interfaceName = interfaceName + private weak var _delegate: PingerDelegate? + private let delegateQueue: DispatchQueue + + var delegate: PingerDelegate? { + get { + stateLock.lock() + defer { stateLock.unlock() } + + return _delegate + } + set { + stateLock.lock() + defer { stateLock.unlock() } + + _delegate = newValue + } } deinit { - stop() + closeSocket() + } + + init(identifier: UInt16 = 757, delegateQueue: DispatchQueue) { + self.identifier = identifier + self.delegateQueue = delegateQueue } - func start(delay: DispatchTimeInterval, repeating repeatInterval: DispatchTimeInterval) throws { + /// Open socket and optionally bind it to the given interface. + /// Automatically closes the previously opened socket when called multiple times in a row. + func openSocket(bindTo interfaceName: String?) throws { stateLock.lock() defer { stateLock.unlock() } - stop() + closeSocket() + + var context = CFSocketContext() + context.info = Unmanaged.passUnretained(self).toOpaque() guard let newSocket = CFSocketCreate( kCFAllocatorDefault, AF_INET, SOCK_DGRAM, IPPROTO_ICMP, - 0, - nil, - nil + CFSocketCallBackType.readCallBack.rawValue, + { socket, callbackType, address, data, info in + guard let socket = socket, let info = info, callbackType == .readCallBack else { + return + } + + let pinger = Unmanaged<Pinger>.fromOpaque(info).takeUnretainedValue() + + pinger.readSocket(socket) + }, + &context ) else { throw Error.createSocket } @@ -56,45 +107,39 @@ final class Pinger { CFSocketSetSocketFlags(newSocket, flags | kCFSocketCloseOnInvalidate) } - try bindSocket(newSocket) + if let interfaceName = interfaceName { + try bindSocket(newSocket, to: interfaceName) + } else { + logger.debug("Interface is not specified.") + } guard let runLoop = CFSocketCreateRunLoopSource(kCFAllocatorDefault, newSocket, 0) else { throw Error.createRunLoop } - CFRunLoopAddSource(CFRunLoopGetMain(), runLoop, .defaultMode) - - let newTimer = DispatchSource.makeTimerSource() - newTimer.setEventHandler { [weak self] in - self?.send() - } + CFRunLoopAddSource(CFRunLoopGetMain(), runLoop, .commonModes) socket = newSocket - timer = newTimer - - newTimer.schedule(wallDeadline: .now() + delay, repeating: repeatInterval) - newTimer.resume() } - func stop() { + func closeSocket() { stateLock.lock() defer { stateLock.unlock() } if let socket = socket { CFSocketInvalidate(socket) - } - socket = nil - - timer?.cancel() - timer = nil + self.socket = nil + } } - private func send() { + /// Send ping packet to the given address. + /// Returns `SendResult` on success, otherwise throws a `Pinger.Error`. + func send(to address: IPv4Address) throws -> SendResult { stateLock.lock() guard let socket = socket else { stateLock.unlock() - return + throw Error.closedSocket } stateLock.unlock() @@ -108,8 +153,7 @@ final class Pinger { let sequenceNumber = nextSequenceNumber() let packetData = Self.createICMPPacket( identifier: identifier, - sequenceNumber: sequenceNumber, - payload: nil + sequenceNumber: sequenceNumber ) let bytesSent = packetData.withUnsafeBytes { dataBuffer -> Int in @@ -127,28 +171,119 @@ final class Pinger { } } - if bytesSent == -1 { - logger.debug("Failed to send echo (errno: \(errno)).") + guard bytesSent >= 0 else { + let errorCode = errno + logger.debug("Failed to send packet (errno: \(errorCode)).") + throw Error.sendPacket(errorCode) } + + return SendResult(sequenceNumber: sequenceNumber, bytesSent: UInt16(bytesSent)) } private func nextSequenceNumber() -> UInt16 { stateLock.lock() - let (partialValue, isOverflow) = sequenceNumber.addingReportingOverflow(1) - let nextSequenceNumber = isOverflow ? 0 : partialValue - - sequenceNumber = nextSequenceNumber + let (nextValue, _) = sequenceNumber.addingReportingOverflow(1) + sequenceNumber = nextValue stateLock.unlock() - return nextSequenceNumber + return nextValue } - private func bindSocket(_ socket: CFSocket) throws { - guard let interfaceName = interfaceName else { - logger.debug("Interface is not specified.") - return + private func readSocket(_ socket: CFSocket) { + var address = sockaddr() + var addressLength = socklen_t(MemoryLayout.size(ofValue: address)) + + let bytesRead = recvfrom( + CFSocketGetNative(socket), + &readBuffer, + Self.bufferSize, + 0, + &address, + &addressLength + ) + + do { + guard bytesRead > 0 else { throw Error.receivePacket(errno) } + + let icmpHeader = try parseICMPResponse(buffer: &readBuffer, length: bytesRead) + guard let sender = Self.makeIPAddress(from: address) else { throw Error.parseIPAddress } + + delegateQueue.async { + self.delegate?.pinger( + self, + didReceiveResponseFromSender: sender, + icmpHeader: icmpHeader + ) + } + } catch Pinger.Error.clientIdentifierMismatch { + // Ignore responses from other senders. + } catch { + delegateQueue.async { + self.delegate?.pinger(self, didFailWithError: error) + } } + } + + private func parseICMPResponse(buffer: inout [UInt8], length: Int) throws -> ICMPHeader { + return try buffer.withUnsafeMutableBytes { bufferPointer in + // Check IP packet size. + guard length >= MemoryLayout<IPv4Header>.size else { + throw Error.malformedResponse(.ipv4PacketTooSmall) + } + + // Verify IPv4 header. + let ipv4Header = bufferPointer.load(as: IPv4Header.self) + let payloadLength = length - ipv4Header.headerLength + guard payloadLength >= MemoryLayout<ICMPHeader>.size else { + throw Error.malformedResponse(.icmpHeaderTooSmall) + } + + guard ipv4Header.isIPv4Version else { + throw Error.malformedResponse(.invalidIPVersion) + } + + // Parse ICMP header. + let icmpHeaderPointer = bufferPointer.baseAddress! + .advanced(by: ipv4Header.headerLength) + .assumingMemoryBound(to: ICMPHeader.self) + + // Check if ICMP response identifier matches the one from sender. + guard icmpHeaderPointer.pointee.identifier.bigEndian == identifier else { + throw Error.clientIdentifierMismatch + } + + // Verify ICMP type. + guard icmpHeaderPointer.pointee.type == ICMP_ECHOREPLY else { + throw Error.malformedResponse(.invalidEchoReplyType) + } + + // Copy server checksum. + let serverChecksum = icmpHeaderPointer.pointee.checksum.bigEndian + + // Reset checksum field before calculating checksum. + icmpHeaderPointer.pointee.checksum = 0 + + // Verify ICMP checksum. + let payloadPointer = UnsafeRawBufferPointer( + start: icmpHeaderPointer, + count: payloadLength + ) + let clientChecksum = in_chksum(payloadPointer) + if clientChecksum != serverChecksum { + throw Error.malformedResponse(.checksumMismatch(clientChecksum, serverChecksum)) + } + + // Ensure endianess before returning ICMP packet to delegate. + var icmpHeader = icmpHeaderPointer.pointee + icmpHeader.identifier = icmpHeader.identifier.bigEndian + icmpHeader.sequenceNumber = icmpHeader.sequenceNumber.bigEndian + icmpHeader.checksum = serverChecksum + return icmpHeader + } + } + + private func bindSocket(_ socket: CFSocket, to interfaceName: String) throws { var index = if_nametoindex(interfaceName) guard index > 0 else { throw Error.mapInterfaceNameToIndex(errno) @@ -165,57 +300,59 @@ final class Pinger { ) if result == -1 { - logger - .error( - "Failed to bind socket to \"\(interfaceName)\" (index: \(index), errno: \(errno))." - ) - + logger.error( + "Failed to bind socket to \"\(interfaceName)\" (index: \(index), errno: \(errno))." + ) throw Error.bindSocket(errno) } } - private class func createICMPPacket( - identifier: UInt16, - sequenceNumber: UInt16, - payload: Data? - ) -> Data { - // Create data buffer. - var data = Data() - - // ICMP type. - data.append(UInt8(ICMP_ECHO)) - - // Code. - data.append(UInt8(0)) - - // Checksum. - withUnsafeBytes(of: UInt16(0)) { data.append(Data($0)) } + private class func createICMPPacket(identifier: UInt16, sequenceNumber: UInt16) -> Data { + var header = ICMPHeader( + type: UInt8(ICMP_ECHO), + code: 0, + checksum: 0, + identifier: identifier.bigEndian, + sequenceNumber: sequenceNumber.bigEndian + ) + header.checksum = withUnsafeBytes(of: &header) { in_chksum($0).bigEndian } - // Identifier. - withUnsafeBytes(of: identifier.bigEndian) { data.append(Data($0)) } + return withUnsafeBytes(of: &header) { Data($0) } + } - // Sequence number. - withUnsafeBytes(of: sequenceNumber.bigEndian) { data.append(Data($0)) } + private class func makeIPAddress(from sa: sockaddr) -> IPAddress? { + if sa.sa_family == AF_INET { + return withUnsafeBytes(of: sa) { buffer -> IPAddress? in + buffer.bindMemory(to: sockaddr_in.self).baseAddress.flatMap { boundPointer in + var saddr = boundPointer.pointee + let data = Data(bytes: &saddr.sin_addr, count: MemoryLayout<in_addr>.size) - // Append payload. - if let payload = payload { - data.append(contentsOf: payload) + return IPv4Address(data, nil) + } + } } - // Calculate checksum. - let checksum = in_chksum(data) + if sa.sa_family == AF_INET6 { + return withUnsafeBytes(of: sa) { buffer in + return buffer.bindMemory(to: sockaddr_in6.self).baseAddress + .flatMap { boundPointer in + var saddr6 = boundPointer.pointee + let data = Data( + bytes: &saddr6.sin6_addr, + count: MemoryLayout<in6_addr>.size + ) - // Inject computed checksum into the packet. - data.withUnsafeMutableBytes { buffer in - buffer.storeBytes(of: checksum, toByteOffset: 2, as: UInt16.self) + return IPv6Address(data) + } + } } - return data + return nil } } extension Pinger { - enum Error: LocalizedError, Equatable { + enum Error: LocalizedError { /// Failure to create a socket. case createSocket @@ -228,6 +365,24 @@ extension Pinger { /// Failure to create a runloop for socket. case createRunLoop + /// Failure to send a packet due to socket being closed. + case closedSocket + + /// Failure to send packet. Contains the `errno`. + case sendPacket(Int32) + + /// Failure to receive packet. Contains the `errno`. + case receivePacket(Int32) + + /// Response identifier does not match the sender identifier. + case clientIdentifierMismatch + + /// Malformed response. + case malformedResponse(MalformedResponseReason) + + /// Failure to parse IP address. + case parseIPAddress + var errorDescription: String? { switch self { case .createSocket: @@ -238,23 +393,55 @@ extension Pinger { return "Failure to bind socket to interface." case .createRunLoop: return "Failure to create run loop for socket." + case .closedSocket: + return "Socket is closed." + case let .sendPacket(code): + return "Failure to send packet (errno: \(code))." + case let .receivePacket(code): + return "Failure to receive packet (errno: \(code))." + case .clientIdentifierMismatch: + return "Response identifier does not match the sender identifier." + case let .malformedResponse(reason): + return "Malformed response: \(reason)." + case .parseIPAddress: + return "Failed to parse IP address." } } } + + enum MalformedResponseReason { + case ipv4PacketTooSmall + case icmpHeaderTooSmall + case invalidIPVersion + case invalidEchoReplyType + case checksumMismatch(UInt16, UInt16) + } } -private func in_chksum(_ data: Data) -> UInt16 { - let words = sequence(state: data.makeIterator()) { iterator in - return iterator.next().map { byte in - return iterator.next().map { nextByte in - return [byte, nextByte].withUnsafeBytes { buffer in - return buffer.load(as: UInt16.self) - } - } ?? UInt16(byte) - } +private func in_chksum<S>(_ data: S) -> UInt16 where S: Sequence, S.Element == UInt8 { + var iterator = data.makeIterator() + var words = [UInt16]() + + while let byte = iterator.next() { + let nextByte = iterator.next() ?? 0 + let word = UInt16(byte) << 8 | UInt16(nextByte) + + words.append(word) } let sum = words.reduce(0, &+) return ~sum } + +private extension IPv4Header { + /// Returns IPv4 header length. + var headerLength: Int { + return Int(versionAndHeaderLength & 0x0F) * MemoryLayout<UInt32>.size + } + + /// Returns `true` if version header indicates IPv4. + var isIPv4Version: Bool { + return (versionAndHeaderLength & 0xF0) == 0x40 + } +} diff --git a/ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift b/ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift index 60ee52a1ed..5cd93800a3 100644 --- a/ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift +++ b/ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift @@ -11,23 +11,219 @@ import Logging import NetworkExtension import WireGuardKit -final class TunnelMonitor { +/// Interval for periodic heartbeat ping issued when traffic is flowing. +/// Should help to detect connectivity issues on networks that drop traffic in one of directions, +/// regardless if tx/rx counters are being updated. +private let heartbeatPingInterval: TimeInterval = 10 + +/// Heartbeat timeout that once exceeded triggers next heartbeat to be sent. +private let heartbeatReplyTimeout: TimeInterval = 3 + +/// Timeout used to determine if there was a network activity lately. +private let trafficFlowTimeout: TimeInterval = heartbeatPingInterval * 0.5 + +/// Ping timeout. +private let pingTimeout: TimeInterval = 15 + +/// Interval to wait before sending next ping. +private let pingDelay: TimeInterval = 3 + +/// Initial timeout when establishing connection. +private let initialEstablishTimeout: TimeInterval = 4 + +/// Multiplier applied to `establishTimeout` on each failed connection attempt. +private let establishTimeoutMultiplier: UInt32 = 2 + +/// Maximum timeout when establishing connection. +private let maxEstablishTimeout: TimeInterval = pingTimeout + +/// Connectivity check periodicity. +private let connectivityCheckInterval: TimeInterval = 1 + +/// Inbound traffic timeout used when outbound traffic was registered prior to inbound traffic. +private let inboundTrafficTimeout: TimeInterval = 5 + +/// Traffic timeout applied when both tx/rx counters remain stale, i.e no traffic flowing. +/// Ping is issued after that timeout is exceeded.s +private let trafficTimeout: TimeInterval = 120 + +final class TunnelMonitor: PingerDelegate { + /// Connection state. + private enum ConnectionState { + /// Initialized and doing nothing. + case stopped + + /// Establishing connection. + case connecting + + /// Connection is established. + case connected + + /// Waiting for network connectivity. + case waitingConnectivity + } + + /// Tunnel monitor state. + private struct State { + /// Current connection state. + var connectionState: ConnectionState = .stopped + + /// Network counters. + var netStats = WgStats() + + /// Ping stats. + var pingStats = PingStats() + + /// Reference date used to determine if timeout has occurred. + var timeoutReference = Date() + + /// Last seen change in rx counter. + var lastSeenRx: Date? + + /// Last seen change in tx counter. + var lastSeenTx: Date? + + /// Whether periodic heartbeat is suspended. + var isHeartbeatSuspended = false + + /// Retry attempt. + var retryAttempt: UInt32 = 0 + + func evaluateConnection(now: Date, pingTimeout: TimeInterval) -> ConnectionEvaluation { + switch connectionState { + case .connecting: + if now.timeIntervalSince(timeoutReference) >= pingTimeout { + return .pingTimeout + } + + guard let lastRequestDate = pingStats.lastRequestDate else { + return .sendInitialPing + } + + if now.timeIntervalSince(lastRequestDate) >= pingDelay { + return .sendNextPing + } + + case .connected: + if now.timeIntervalSince(timeoutReference) >= pingTimeout, !isHeartbeatSuspended { + return .pingTimeout + } + + guard let lastRequestDate = pingStats.lastRequestDate else { + return .sendInitialPing + } + + let timeSinceLastPing = now.timeIntervalSince(lastRequestDate) + if let lastReplyDate = pingStats.lastReplyDate, + lastRequestDate.timeIntervalSince(lastReplyDate) >= heartbeatReplyTimeout, + timeSinceLastPing >= pingDelay + { + return .retryHeartbeatPing + } + + guard let lastSeenRx = lastSeenRx, let lastSeenTx = lastSeenTx else { return .ok } + + let rxTimeElapsed = now.timeIntervalSince(lastSeenRx) + let txTimeElapsed = now.timeIntervalSince(lastSeenTx) + + if timeSinceLastPing >= heartbeatPingInterval { + // Send heartbeat if traffic is flowing. + if rxTimeElapsed <= trafficFlowTimeout || txTimeElapsed <= trafficFlowTimeout { + return .sendHeartbeatPing + } + + if !isHeartbeatSuspended { + return .suspendHeartbeat + } + } + + if timeSinceLastPing >= pingDelay { + if txTimeElapsed >= trafficTimeout || rxTimeElapsed >= trafficTimeout { + return .trafficTimeout + } + + if lastSeenTx > lastSeenRx, rxTimeElapsed >= inboundTrafficTimeout { + return .inboundTrafficTimeout + } + } + + default: + break + } + + return .ok + } + + func getPingTimeout() -> TimeInterval { + switch connectionState { + case .connecting: + let multiplier = establishTimeoutMultiplier.saturatingPow(retryAttempt) + let nextTimeout = initialEstablishTimeout * Double(multiplier) + + if nextTimeout.isFinite, nextTimeout < maxEstablishTimeout { + return nextTimeout + } else { + return maxEstablishTimeout + } + + case .connected, .waitingConnectivity, .stopped: + return pingTimeout + } + } + + mutating func updateNetStats(newStats: WgStats, now: Date) { + if newStats.bytesReceived > netStats.bytesReceived { + lastSeenRx = now + } + + if newStats.bytesSent > netStats.bytesSent { + lastSeenTx = now + } + + netStats = newStats + } + + mutating func updatePingStats(sendResult: Pinger.SendResult, now: Date) { + pingStats.requests.updateValue(now, forKey: sendResult.sequenceNumber) + pingStats.lastRequestDate = now + } + + mutating func setPingReplyReceived(_ sequenceNumber: UInt16, now: Date) -> Date? { + guard let pingTimestamp = pingStats.requests.removeValue(forKey: sequenceNumber) else { + return nil + } + + pingStats.lastReplyDate = now + timeoutReference = now + + return pingTimestamp + } + } + + /// Ping statistics. + private struct PingStats { + /// Dictionary holding sequence and the corresponding date when ech request took place. + var requests = [UInt16: Date]() + + /// Timestamp when last echo request was sent. + var lastRequestDate: Date? + + /// Timestamp when last echo reply was received. + var lastReplyDate: Date? + } + private let adapter: WireGuardAdapter private let internalQueue = DispatchQueue(label: "TunnelMonitor") private let delegateQueue: DispatchQueue - private var address: IPv4Address? - private var pinger: Pinger? + private let pinger: Pinger private var pathMonitor: NWPathMonitor? - private var networkBytesReceived: UInt64 = 0 - private var firstAttemptDate: Date? - private var lastAttemptDate: Date? - private var lastError: Pinger.Error? - private var isStarted = false - private var isPinging = false + private var timer: DispatchSourceTimer? + + private var state = State() + private var probeAddress: IPv4Address? private var logger = Logger(label: "TunnelMonitor") - private var timer: DispatchSourceTimer? private weak var _delegate: TunnelMonitorDelegate? weak var delegate: TunnelMonitorDelegate? { @@ -43,243 +239,362 @@ final class TunnelMonitor { } } - var startDate: Date? { - return internalQueue.sync { - return firstAttemptDate - } - } - - init(queue: DispatchQueue, adapter: WireGuardAdapter) { + init(queue: DispatchQueue, adapter anAdapter: WireGuardAdapter) { delegateQueue = queue - self.adapter = adapter + adapter = anAdapter + + pinger = Pinger(delegateQueue: internalQueue) + pinger.delegate = self } deinit { - stopNoQueue(forRestart: false) + stopNoQueue() } - func start(address: IPv4Address) { + func start(probeAddress: IPv4Address) { internalQueue.async { - self.startNoQueue(address: address) + self.startNoQueue(probeAddress: probeAddress) } } func stop() { internalQueue.async { - self.stopNoQueue(forRestart: false) + self.stopNoQueue() } } - private func startNoQueue(address pingAddress: IPv4Address) { - if isStarted { - logger.debug("Restart tunnel monitor with address: \(pingAddress).") + // MARK: - PingerDelegate + + func pinger( + _ pinger: Pinger, + didReceiveResponseFromSender senderAddress: IPAddress, + icmpHeader: ICMPHeader + ) { + didReceivePing(from: senderAddress, icmpHeader: icmpHeader) + } + + func pinger(_ pinger: Pinger, didFailWithError error: Error) { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to parse ICMP response." + ) + } + + // MARK: - Private + + private func startNoQueue(probeAddress: IPv4Address) { + if case .stopped = state.connectionState { + logger.debug("Start with address: \(probeAddress).") } else { - logger.debug("Start tunnel monitor with address: \(pingAddress).") + stopNoQueue(forRestart: true) + logger.debug("Restart with address: \(probeAddress)") } - stopNoQueue(forRestart: true) - - isStarted = true - address = pingAddress - networkBytesReceived = 0 - firstAttemptDate = Date() - lastAttemptDate = firstAttemptDate - lastError = nil + self.probeAddress = probeAddress - let newPathMonitor = NWPathMonitor() - newPathMonitor.pathUpdateHandler = { [weak self] path in + let pathMonitor = NWPathMonitor() + pathMonitor.pathUpdateHandler = { [weak self] path in self?.handleNetworkPathUpdate(path) } - newPathMonitor.start(queue: internalQueue) - pathMonitor = newPathMonitor + pathMonitor.start(queue: internalQueue) + self.pathMonitor = pathMonitor + + if isNetworkPathReachable(pathMonitor.currentPath) { + logger.debug("Start monitoring connection.") + + startMonitoring() + } else { + logger.debug("Wait for network to become reachable before starting monitoring.") - handleNetworkPathUpdate(newPathMonitor.currentPath) + state.connectionState = .waitingConnectivity + } } - private func stopNoQueue(forRestart: Bool) { - if isStarted, !forRestart { + private func stopNoQueue(forRestart: Bool = false) { + if case .stopped = state.connectionState { + return + } + + if !forRestart { logger.debug("Stop tunnel monitor.") } - isStarted = false - address = nil - firstAttemptDate = nil - lastAttemptDate = nil - lastError = nil + probeAddress = nil pathMonitor?.cancel() pathMonitor = nil - cancelWgStatsTimer() - stopPinging() + stopMonitoring(resetRetryAttempt: !forRestart) + + state.connectionState = .stopped } - private func startPinging(address: IPv4Address) throws { - let newPinger = Pinger(address: address, interfaceName: adapter.interfaceName) + private func checkConnectivity() { + guard let probeAddress = probeAddress, let newStats = getStats() else { + return + } - try newPinger.start( - delay: TunnelMonitorConfiguration.pingStartDelay, - repeating: TunnelMonitorConfiguration.pingInterval - ) + // Check if counters were reset. + let isStatsReset = newStats.bytesReceived < state.netStats.bytesReceived || + newStats.bytesSent < state.netStats.bytesSent - pinger = newPinger - isPinging = true - } + guard !isStatsReset else { + state.netStats = newStats + return + } - private func stopPinging() { - pinger?.stop() - pinger = nil + #if DEBUG + logCounters(currentStats: state.netStats, newStats: newStats) + #endif - isPinging = false - } + let now = Date() + state.updateNetStats(newStats: newStats, now: now) - private func setWgStatsTimer() { - // Cancel existing timer. - cancelWgStatsTimer() + let timeout = state.getPingTimeout() + let evaluation = state.evaluateConnection(now: now, pingTimeout: timeout) - // Create new timer. - timer = DispatchSource.makeTimerSource(queue: internalQueue) - timer?.setEventHandler { [weak self] in - self?.onWgStatsTimer() + if evaluation != .ok { + logger.debug("Evaluation: \(evaluation)") } - timer?.schedule( - wallDeadline: .now(), - repeating: TunnelMonitorConfiguration.wgStatsQueryInterval - ) - timer?.resume() - logger.debug("Set WG stats timer.") + switch evaluation { + case .ok: + break + + case .pingTimeout: + startConnectionRecovery() + + case .suspendHeartbeat: + state.isHeartbeatSuspended = true + + case .sendHeartbeatPing, .retryHeartbeatPing, .sendNextPing, .sendInitialPing, + .inboundTrafficTimeout, .trafficTimeout: + if state.isHeartbeatSuspended { + state.isHeartbeatSuspended = false + state.timeoutReference = now + } + sendPing(to: probeAddress, now: now) + } } - private func cancelWgStatsTimer() { - timer?.cancel() - timer = nil + #if DEBUG + private func logCounters(currentStats: WgStats, newStats: WgStats) { + let rxDelta = newStats.bytesReceived.saturatingSubtraction(currentStats.bytesReceived) + let txDelta = newStats.bytesSent.saturatingSubtraction(currentStats.bytesSent) + + guard rxDelta > 0 || txDelta > 0 else { return } + + logger.debug( + """ + rx: \(newStats.bytesReceived) (+\(rxDelta)) \ + tx: \(newStats.bytesSent) (+\(txDelta)) + """ + ) } + #endif + + private func startConnectionRecovery() { + stopConnectivityCheckTimer() - private func onWgStatsTimer() { - adapter.getRuntimeConfiguration { [weak self] str in + state.pingStats = PingStats() + state.isHeartbeatSuspended = false + state.retryAttempt = state.retryAttempt.saturatingAddition(1) + + sendDelegateShouldHandleConnectionRecovery { [weak self] in guard let self = self else { return } self.internalQueue.async { - self.handleWgStatsUpdate(string: str) + switch self.state.connectionState { + case .connecting, .connected: + self.startConnectivityCheckTimer() + + case .stopped, .waitingConnectivity: + break + } } } } - private func handleWgStatsUpdate(string: String?) { - guard let string = string else { - logger.debug("Received no runtime configuration from WireGuard adapter.") - return + private func sendPing(to receiver: IPv4Address, now: Date) { + do { + let sendResult = try pinger.send(to: receiver) + state.updatePingStats(sendResult: sendResult, now: now) + + logger.debug("Send ping icmp_seq=\(sendResult.sequenceNumber).") + } catch { + logger.error(chainedError: AnyChainedError(error), message: "Failed to send ping.") } + } - guard let newNetworkBytesReceived = Self.parseNetworkBytesReceived(from: string) else { - logger.debug("Failed to parse rx_bytes from runtime configuration.") - return + private func handleNetworkPathUpdate(_ networkPath: Network.NWPath) { + let isReachable = isNetworkPathReachable(networkPath) + + switch (isReachable, state.connectionState) { + case (true, .waitingConnectivity): + logger.debug("Network is reachable. Resume monitoring.") + + startMonitoring() + sendDelegateNetworkStatusChange(isReachable) + + case (false, .connecting), (false, .connected): + logger.debug("Network is unreachable. Pause monitoring.") + + state.connectionState = .waitingConnectivity + stopMonitoring(resetRetryAttempt: true) + sendDelegateNetworkStatusChange(isReachable) + + default: + break } + } - let oldNetworkBytesReceived = networkBytesReceived - networkBytesReceived = newNetworkBytesReceived + private func didReceivePing(from sender: IPAddress, icmpHeader: ICMPHeader) { + guard let probeAddress = probeAddress else { return } - if newNetworkBytesReceived < oldNetworkBytesReceived { - logger - .debug( - "Stats was reset? newNetworkBytesReceived = \(newNetworkBytesReceived), oldNetworkBytesReceived = \(oldNetworkBytesReceived)" - ) + if sender.rawValue != probeAddress.rawValue { + logger.debug("Got reply from unknown sender: \(sender), expected: \(probeAddress).") + } + + let now = Date() + let sequenceNumber = icmpHeader.sequenceNumber + guard let pingTimestamp = state.setPingReplyReceived(sequenceNumber, now: now) else { + logger.debug("Got unknown ping sequence: \(sequenceNumber).") return } - if newNetworkBytesReceived > oldNetworkBytesReceived { - // Tell delegate that connection is established. - delegateQueue.async { - self.delegate?.tunnelMonitorDidDetermineConnectionEstablished(self) - } + let time = now.timeIntervalSince(pingTimestamp) * 1000 + let message = String( + format: "Received reply icmp_seq=%d, time=%.2f ms.", + sequenceNumber, + time + ) + logger.debug(.init(stringLiteral: message)) + + if case .connecting = state.connectionState { + state.connectionState = .connected + state.retryAttempt = 0 + sendDelegateConnectionEstablished() + } + } - // Stop the tunnel monitor. - stopNoQueue(forRestart: false) + private func startMonitoring() { + do { + guard let interfaceName = adapter.interfaceName else { + logger.debug("Failed to obtain utun interface name.") + return + } + try pinger.openSocket(bindTo: interfaceName) + } catch { + logger.error(chainedError: AnyChainedError(error), message: "Failed to open socket.") return } - if let nextAttemptDate = lastAttemptDate? - .addingTimeInterval(TunnelMonitorConfiguration.connectionTimeout), - nextAttemptDate <= Date() - { - // Reset the last recovery attempt date. - lastAttemptDate = nextAttemptDate + state.connectionState = .connecting - // Reset last error. - lastError = nil + startConnectivityCheckTimer() + } - // Tell delegate to attempt the connection recovery. - delegateQueue.async { - self.delegate?.tunnelMonitorDelegateShouldHandleConnectionRecovery(self) - } + private func stopMonitoring(resetRetryAttempt: Bool) { + stopConnectivityCheckTimer() + pinger.closeSocket() + + state.netStats = WgStats() + state.lastSeenRx = nil + state.lastSeenTx = nil + state.pingStats = PingStats() + + if resetRetryAttempt { + state.retryAttempt = 0 } + + state.isHeartbeatSuspended = false } - private func handleNetworkPathUpdate(_ networkPath: Network.NWPath) { - guard let address = address else { - return + private func startConnectivityCheckTimer() { + let timer = DispatchSource.makeTimerSource(queue: internalQueue) + timer.setEventHandler { [weak self] in + self?.checkConnectivity() } + timer.schedule(wallDeadline: .now(), repeating: connectivityCheckInterval) + timer.activate() - let isNetworkReachable = isNetworkPathReachable(networkPath) + self.timer?.cancel() + self.timer = timer - switch (isNetworkReachable, isPinging) { - case (true, false): - logger.debug("Network is reachable. Starting to ping.") + state.timeoutReference = Date() + } - do { - try startPinging(address: address) + private func stopConnectivityCheckTimer() { + timer?.cancel() + timer = nil + } - // Reset the last recovery attempt date. - firstAttemptDate = Date() - lastAttemptDate = firstAttemptDate + private func sendDelegateConnectionEstablished() { + delegateQueue.async { + self.delegate?.tunnelMonitorDidDetermineConnectionEstablished(self) + } + } - // Start WG stats timer. - setWgStatsTimer() + private func sendDelegateShouldHandleConnectionRecovery(completion: @escaping () -> Void) { + delegateQueue.async { + self.delegate?.tunnelMonitorDelegate( + self, + shouldHandleConnectionRecoveryWithCompletion: completion + ) + } + } - delegateQueue.async { - self.delegate?.tunnelMonitor( - self, - networkReachabilityStatusDidChange: isNetworkReachable - ) - } - } catch { - let error = error as! Pinger.Error + private func sendDelegateNetworkStatusChange(_ isNetworkReachable: Bool) { + delegateQueue.async { + self.delegate?.tunnelMonitor( + self, + networkReachabilityStatusDidChange: isNetworkReachable + ) + } + } - if error != lastError { - logger.error( - chainedError: AnyChainedError(error), - message: "Failed to start pinging." - ) - lastError = error - } - } + private enum ConnectionEvaluation { + case ok + case sendInitialPing + case sendNextPing + case sendHeartbeatPing + case retryHeartbeatPing + case suspendHeartbeat + case inboundTrafficTimeout + case trafficTimeout + case pingTimeout + } - case (false, true): - logger.debug("Network is unreachable. Stop pinging and wait...") + private func getStats() -> WgStats? { + var result: String? - // Cancel timers and ping. - cancelWgStatsTimer() - stopPinging() + let dispatchGroup = DispatchGroup() + dispatchGroup.enter() + adapter.getRuntimeConfiguration { string in + result = string + dispatchGroup.leave() + } - // Reset the last recovery attempt date. - lastAttemptDate = nil + guard case .success = dispatchGroup.wait(wallTimeout: .now() + .seconds(1)) else { + logger.debug("adapter.getRuntimeConfiguration timeout.") + return nil + } - delegateQueue.async { - self.delegate?.tunnelMonitor( - self, - networkReachabilityStatusDidChange: isNetworkReachable - ) - } + guard let result = result else { + logger.debug("Received nil string for stats.") + return nil + } - default: - break + guard let newStats = WgStats(from: result) else { + logger.debug("Couldn't parse stats.") + return nil } + + return newStats } private func isNetworkPathReachable(_ networkPath: Network.NWPath) -> Bool { - // Get utun interface name. guard let tunName = adapter.interfaceName else { return false } // Check if utun is up. @@ -287,7 +602,6 @@ final class TunnelMonitor { return interface.name == tunName } - // Return false if tunnel is down. guard utunUp else { return false } @@ -306,19 +620,4 @@ final class TunnelMonitor { return false } } - - private class func parseNetworkBytesReceived(from string: String) -> UInt64? { - guard let range = string.range(of: "rx_bytes=") else { return nil } - - let startIndex = range.upperBound - let endIndex = string[startIndex...].firstIndex { ch in - return ch.isNewline - } - - if let endIndex = endIndex { - return UInt64(string[startIndex ..< endIndex]) - } else { - return nil - } - } } diff --git a/ios/PacketTunnel/TunnelMonitor/TunnelMonitorConfiguration.swift b/ios/PacketTunnel/TunnelMonitor/TunnelMonitorConfiguration.swift deleted file mode 100644 index 13f4c4908d..0000000000 --- a/ios/PacketTunnel/TunnelMonitor/TunnelMonitorConfiguration.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// TunnelMonitorConfiguration.swift -// PacketTunnel -// -// Created by pronebird on 10/03/2022. -// Copyright © 2022 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -enum TunnelMonitorConfiguration { - /// Interval at which to query the adapter for stats. - static let wgStatsQueryInterval: DispatchTimeInterval = .milliseconds(50) - - /// Interval for sending echo packets. - static let pingInterval: DispatchTimeInterval = .seconds(3) - - /// Delay before sending the first echo packet. - static let pingStartDelay: DispatchTimeInterval = .milliseconds(500) - - /// Interval after which connection is treated as being lost. - static let connectionTimeout: TimeInterval = 15 -} diff --git a/ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift b/ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift index 9839faf3e5..51c47419c1 100644 --- a/ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift +++ b/ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift @@ -13,7 +13,10 @@ protocol TunnelMonitorDelegate: AnyObject { func tunnelMonitorDidDetermineConnectionEstablished(_ tunnelMonitor: TunnelMonitor) /// Invoked when tunnel monitor determined that connection attempt has failed. - func tunnelMonitorDelegateShouldHandleConnectionRecovery(_ tunnelMonitor: TunnelMonitor) + func tunnelMonitorDelegate( + _ tunnelMonitor: TunnelMonitor, + shouldHandleConnectionRecoveryWithCompletion completionHandler: @escaping () -> Void + ) /// Invoked when network reachability status changes. func tunnelMonitor( diff --git a/ios/PacketTunnel/TunnelMonitor/WgStats.swift b/ios/PacketTunnel/TunnelMonitor/WgStats.swift new file mode 100644 index 0000000000..228b89e38a --- /dev/null +++ b/ios/PacketTunnel/TunnelMonitor/WgStats.swift @@ -0,0 +1,51 @@ +// +// WgStats.swift +// PacketTunnel +// +// Created by pronebird on 08/08/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +struct WgStats { + let bytesReceived: UInt64 + let bytesSent: UInt64 + + init() { + bytesReceived = 0 + bytesSent = 0 + } + + init?(from string: String) { + var _bytesReceived: UInt64? + var _bytesSent: UInt64? + + string.enumerateLines { line, stop in + if _bytesReceived == nil, let value = parseValue("rx_bytes=", in: line) { + _bytesReceived = value + } else if _bytesSent == nil, let value = parseValue("tx_bytes=", in: line) { + _bytesSent = value + } + + if _bytesReceived != nil, _bytesSent != nil { + stop = true + } + } + + guard let _bytesReceived = _bytesReceived, let _bytesSent = _bytesSent else { + return nil + } + + bytesReceived = _bytesReceived + bytesSent = _bytesSent + } +} + +@inline(__always) private func parseValue(_ prefixKey: String, in line: String) -> UInt64? { + guard line.hasPrefix(prefixKey) else { return nil } + + let value = line.dropFirst(prefixKey.count) + + return UInt64(value) +} |
