summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/.swiftformat1
-rw-r--r--ios/CHANGELOG.md2
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj54
-rw-r--r--ios/MullvadVPN/AccountViewController.swift2
-rw-r--r--ios/MullvadVPN/AppDelegate.swift8
-rw-r--r--ios/MullvadVPN/Bundle+ProductVersion.swift12
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift35
-rw-r--r--ios/MullvadVPN/Logging/Logging.swift2
-rw-r--r--ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift2
-rw-r--r--ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift108
-rw-r--r--ios/MullvadVPN/Notifications/TunnelStatusNotificationProvider.swift198
-rw-r--r--ios/MullvadVPN/Operations/BackgroundObserver.swift64
-rw-r--r--ios/MullvadVPN/OutOfTimeViewController.swift4
-rw-r--r--ios/MullvadVPN/PreferencesViewController.swift2
-rw-r--r--ios/MullvadVPN/RevokedDeviceViewController.swift4
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift4
-rw-r--r--ios/MullvadVPN/SettingsDataSource.swift2
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider.swift554
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProviderHost.swift156
-rw-r--r--ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift67
-rw-r--r--ios/MullvadVPN/TunnelManager/PacketTunnelStatus.swift6
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift5
-rw-r--r--ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift12
-rw-r--r--ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift8
-rw-r--r--ios/MullvadVPN/TunnelManager/Tunnel.swift4
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelInteractor.swift15
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift172
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelObserver.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState.swift52
-rw-r--r--ios/MullvadVPNTests/FixedWidthIntegerArithmeticsTests.swift36
-rw-r--r--ios/PacketTunnel/FixedWidthInteger+Arithmetics.swift65
-rw-r--r--ios/PacketTunnel/ObjCBridgingHeader.h15
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift213
-rw-r--r--ios/PacketTunnel/TunnelMonitor/ICMPHeader.h24
-rw-r--r--ios/PacketTunnel/TunnelMonitor/IPv4Header.h33
-rw-r--r--ios/PacketTunnel/TunnelMonitor/Pinger.swift367
-rw-r--r--ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift651
-rw-r--r--ios/PacketTunnel/TunnelMonitor/TunnelMonitorConfiguration.swift23
-rw-r--r--ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift5
-rw-r--r--ios/PacketTunnel/TunnelMonitor/WgStats.swift51
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)
+}