summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-03-17 17:00:56 +0100
committerAndrej Mihajlov <and@mullvad.net>2022-03-17 17:00:56 +0100
commit55cf6fc80cbc60ffe992ee254833f672cb4ac0e7 (patch)
tree16d765a865efb760ba4a38d039880003a1aeeba1
parent92304fd9f9436209531aaa78125f843f918f7803 (diff)
parent1724aa75ae1f8960c8ac18b03421fc94833485b2 (diff)
downloadmullvadvpn-55cf6fc80cbc60ffe992ee254833f672cb4ac0e7.tar.xz
mullvadvpn-55cf6fc80cbc60ffe992ee254833f672cb4ac0e7.zip
Merge branch 'tunnel-conn-monitor'
-rw-r--r--ios/CHANGELOG.md7
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj20
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift32
-rw-r--r--ios/MullvadVPN/Logging/LogFormatting.swift2
-rw-r--r--ios/MullvadVPN/RelaySelector.swift12
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProviderHost.swift56
-rw-r--r--ios/MullvadVPN/Tunnel.swift198
-rw-r--r--ios/MullvadVPN/TunnelIPC/PacketTunnelStatus.swift36
-rw-r--r--ios/MullvadVPN/TunnelIPC/TunnelConnectionInfo.swift17
-rw-r--r--ios/MullvadVPN/TunnelIPC/TunnelIPCRequest.swift8
-rw-r--r--ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift137
-rw-r--r--ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift20
-rw-r--r--ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift52
-rw-r--r--ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift4
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift8
-rw-r--r--ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift8
-rw-r--r--ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift14
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift206
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManagerState.swift41
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState.swift69
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift77
22 files changed, 746 insertions, 280 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md
index e4d5550a11..9cefde6b87 100644
--- a/ios/CHANGELOG.md
+++ b/ios/CHANGELOG.md
@@ -24,6 +24,13 @@ Line wrap the file at 100 chars. Th
## [Unreleased]
### Added
+- Add tunnel monitor when establishing tunnel connection. Picks next relay every 15 seconds until
+ any inbound traffic received. This should also keep the tunnel in connecting or reconnecting state
+ until the tunnel monitor determined that connection is functional.
+
+
+## [2022.1] - 2022-02-15
+### Added
- Show privacy overlay when entering app switcher.
- Add option to block malware.
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 49ce76af67..427ae1c59e 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -151,11 +151,11 @@
585DA89426B0323E00B8C587 /* TunnelIPCRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89226B0323E00B8C587 /* TunnelIPCRequest.swift */; };
585DA89626B0328000B8C587 /* TunnelIPCResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89526B0328000B8C587 /* TunnelIPCResponse.swift */; };
585DA89726B0328000B8C587 /* TunnelIPCResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89526B0328000B8C587 /* TunnelIPCResponse.swift */; };
- 585DA89926B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */; };
- 585DA89A26B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */; };
+ 585DA89926B0329200B8C587 /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; };
+ 585DA89A26B0329200B8C587 /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; };
585DA89B26B146B300B8C587 /* TunnelIPCCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88E26B031E200B8C587 /* TunnelIPCCoding.swift */; };
585DA8A326B14E0D00B8C587 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; };
- 585DA8A526B14EE000B8C587 /* TunnelConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */; };
+ 585DA8A526B14EE000B8C587 /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; };
585DA8A626B14F5100B8C587 /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; };
5860392926DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; };
5860392A26DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; };
@@ -270,6 +270,7 @@
58D0C7A223F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */; };
58D67A0A26D7AE3300557C3C /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; };
58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */; };
+ 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0A98727C8F46300FE6BDD /* Tunnel.swift */; };
58E1336926D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; };
58E1336A26D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; };
58E1336B26D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; };
@@ -461,7 +462,7 @@
585DA88E26B031E200B8C587 /* TunnelIPCCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCCoding.swift; sourceTree = "<group>"; };
585DA89226B0323E00B8C587 /* TunnelIPCRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCRequest.swift; sourceTree = "<group>"; };
585DA89526B0328000B8C587 /* TunnelIPCResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCResponse.swift; sourceTree = "<group>"; };
- 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelConnectionInfo.swift; sourceTree = "<group>"; };
+ 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelStatus.swift; sourceTree = "<group>"; };
585DA8AE26B9492500B8C587 /* Promise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = "<group>"; };
5860392826DCE7AB00554C79 /* PromiseCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseCompletion.swift; sourceTree = "<group>"; };
5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = "<group>"; };
@@ -557,6 +558,7 @@
58D0C79F23F1CECF00FE9BA7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MullvadVPNScreenshots.swift; sourceTree = "<group>"; };
58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentManager.swift; sourceTree = "<group>"; };
+ 58E0A98727C8F46300FE6BDD /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = "<group>"; };
58E1336826D2BE3700CC316B /* PromiseObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseObserver.swift; sourceTree = "<group>"; };
58E1336C26D2BE7500CC316B /* AnyResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyResult.swift; sourceTree = "<group>"; };
58E1337026D2BE9C00CC316B /* AnyOptional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyOptional.swift; sourceTree = "<group>"; };
@@ -798,7 +800,7 @@
585DA88D26B031D100B8C587 /* TunnelIPC */ = {
isa = PBXGroup;
children = (
- 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */,
+ 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */,
5845F841236CBACD00B2D93C /* TunnelIPC.swift */,
585DA88E26B031E200B8C587 /* TunnelIPCCoding.swift */,
5875960626F36B3A00BF6711 /* TunnelIPCError.swift */,
@@ -977,6 +979,7 @@
5807E2BF2432038B00F5FF30 /* String+Split.swift */,
5871FB8225498CA20051A0A4 /* Swizzle.swift */,
5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */,
+ 58E0A98727C8F46300FE6BDD /* Tunnel.swift */,
585DA88D26B031D100B8C587 /* TunnelIPC */,
5823FA5726CE4A4100283BF8 /* TunnelManager */,
587AD7C523421D7000E93A53 /* TunnelSettings.swift */,
@@ -1333,7 +1336,7 @@
58A94AE626D23C3D001CB97C /* PromiseTests.swift in Sources */,
5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */,
582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */,
- 585DA8A526B14EE000B8C587 /* TunnelConnectionInfo.swift in Sources */,
+ 585DA8A526B14EE000B8C587 /* PacketTunnelStatus.swift in Sources */,
588DD76D26FCB4A2006F6233 /* Cancellable.swift in Sources */,
5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */,
58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */,
@@ -1359,7 +1362,7 @@
58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */,
5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */,
587B75412668FD7800DEF7E9 /* AccountExpiryNotificationProvider.swift in Sources */,
- 585DA89926B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */,
+ 585DA89926B0329200B8C587 /* PacketTunnelStatus.swift in Sources */,
58BA692E23E99EFF009DC256 /* Locking.swift in Sources */,
5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */,
5840250122B1124600E4CFEC /* IPAddress+Codable.swift in Sources */,
@@ -1494,6 +1497,7 @@
5815039D24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */,
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
588DD76B26FCB49E006F6233 /* Cancellable.swift in Sources */,
+ 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */,
58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */,
5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */,
5840BE35279EDB16002836BA /* OperationCompletion.swift in Sources */,
@@ -1577,7 +1581,7 @@
5838318B27C40A3900000571 /* Pinger.swift in Sources */,
5820675C26E6576800655B05 /* RelayCache.swift in Sources */,
58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */,
- 585DA89A26B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */,
+ 585DA89A26B0329200B8C587 /* PacketTunnelStatus.swift in Sources */,
585DA88526B0270700B8C587 /* ServerRelaysResponse.swift in Sources */,
5806767627048E7D00C858CB /* Promise+Result.swift in Sources */,
581503A724D6F4AE00C9C50E /* Logging.swift in Sources */,
diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift
index 03ca5a2563..59978ec4ad 100644
--- a/ios/MullvadVPN/ConnectViewController.swift
+++ b/ios/MullvadVPN/ConnectViewController.swift
@@ -208,30 +208,30 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
private func updateTunnelConnectionInfo() {
switch tunnelState {
- case .connecting(let connectionInfo):
- setConnectionInfo(connectionInfo)
+ case .connecting(let tunnelRelay):
+ setTunnelRelay(tunnelRelay)
- case .connected(let connectionInfo), .reconnecting(let connectionInfo):
- setConnectionInfo(connectionInfo)
+ case .connected(let tunnelRelay), .reconnecting(let tunnelRelay):
+ setTunnelRelay(tunnelRelay)
case .disconnected, .disconnecting, .pendingReconnect:
- setConnectionInfo(nil)
+ setTunnelRelay(nil)
}
mainContentView.locationContainerView.accessibilityLabel = tunnelState.localizedAccessibilityLabel
}
- private func setConnectionInfo(_ connectionInfo: TunnelConnectionInfo?) {
- if let connectionInfo = connectionInfo {
- mainContentView.cityLabel.attributedText = attributedStringForLocation(string: connectionInfo.location.city)
- mainContentView.countryLabel.attributedText = attributedStringForLocation(string: connectionInfo.location.country)
+ private func setTunnelRelay(_ tunnelRelay: PacketTunnelRelay?) {
+ if let tunnelRelay = tunnelRelay {
+ mainContentView.cityLabel.attributedText = attributedStringForLocation(string: tunnelRelay.location.city)
+ mainContentView.countryLabel.attributedText = attributedStringForLocation(string: tunnelRelay.location.country)
mainContentView.connectionPanel.dataSource = ConnectionPanelData(
- inAddress: "\(connectionInfo.ipv4Relay) UDP",
+ inAddress: "\(tunnelRelay.ipv4Relay) UDP",
outAddress: nil
)
mainContentView.connectionPanel.isHidden = false
- mainContentView.connectionPanel.connectedRelayName = connectionInfo.hostname
+ mainContentView.connectionPanel.connectedRelayName = tunnelRelay.hostname
} else {
mainContentView.countryLabel.attributedText = attributedStringForLocation(string: " ")
mainContentView.cityLabel.attributedText = attributedStringForLocation(string: " ")
@@ -274,15 +274,15 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
private func updateLocation(animated: Bool) {
switch tunnelState {
- case .connecting(let connectionInfo):
- if let connectionInfo = connectionInfo {
- setLocation(coordinate: connectionInfo.location.geoCoordinate, animated: animated)
+ case .connecting(let tunnelRelay):
+ if let tunnelRelay = tunnelRelay {
+ setLocation(coordinate: tunnelRelay.location.geoCoordinate, animated: animated)
} else {
unsetLocation(animated: animated)
}
- case .connected(let connectionInfo), .reconnecting(let connectionInfo):
- setLocation(coordinate: connectionInfo.location.geoCoordinate, animated: animated)
+ case .connected(let tunnelRelay), .reconnecting(let tunnelRelay):
+ setLocation(coordinate: tunnelRelay.location.geoCoordinate, animated: animated)
case .disconnected, .disconnecting, .pendingReconnect:
unsetLocation(animated: animated)
diff --git a/ios/MullvadVPN/Logging/LogFormatting.swift b/ios/MullvadVPN/Logging/LogFormatting.swift
index 2ccbdf4fc6..c41c2b758b 100644
--- a/ios/MullvadVPN/Logging/LogFormatting.swift
+++ b/ios/MullvadVPN/Logging/LogFormatting.swift
@@ -11,7 +11,7 @@ import Foundation
extension Date {
func logFormatDate() -> String {
let formatter = DateFormatter()
- formatter.dateFormat = "dd/MM/yyyy @ HH:mm"
+ formatter.dateFormat = "dd/MM/yyyy @ HH:mm:ss"
return formatter.string(from: self)
}
diff --git a/ios/MullvadVPN/RelaySelector.swift b/ios/MullvadVPN/RelaySelector.swift
index f0bb12ce7f..fdf587e6d4 100644
--- a/ios/MullvadVPN/RelaySelector.swift
+++ b/ios/MullvadVPN/RelaySelector.swift
@@ -21,12 +21,12 @@ private struct RelayWithLocation {
}
extension RelaySelectorResult {
- var tunnelConnectionInfo: TunnelConnectionInfo {
- return TunnelConnectionInfo(
- ipv4Relay: self.endpoint.ipv4Relay,
- ipv6Relay: self.endpoint.ipv6Relay,
- hostname: self.relay.hostname,
- location: self.location
+ var packetTunnelRelay: PacketTunnelRelay {
+ return PacketTunnelRelay(
+ ipv4Relay: endpoint.ipv4Relay,
+ ipv6Relay: endpoint.ipv6Relay,
+ hostname: relay.hostname,
+ location: location
)
}
}
diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift
index cc1cbeef52..7305dbe0f2 100644
--- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift
@@ -14,7 +14,7 @@ import Logging
class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
- private var connectionInfo: TunnelConnectionInfo?
+ private var tunnelStatus = PacketTunnelStatus()
private let providerLogger = Logger(label: "SimulatorTunnelProviderHost")
private let stateQueue = DispatchQueue(label: "SimulatorTunnelProviderHostQueue")
@@ -31,9 +31,9 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
}
if let appSelectorResult = appSelectorResult.flattenValue {
- self.connectionInfo = appSelectorResult.tunnelConnectionInfo
+ self.tunnelStatus.tunnelRelay = appSelectorResult.packetTunnelRelay
} else {
- self.connectionInfo = self.pickRelay()?.tunnelConnectionInfo
+ self.tunnelStatus.tunnelRelay = self.pickRelay()?.packetTunnelRelay
}
completionHandler(nil)
@@ -42,41 +42,41 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
stateQueue.async {
- self.connectionInfo = nil
+ self.tunnelStatus = PacketTunnelStatus()
completionHandler()
}
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
- Result { try TunnelIPC.Coding.decodeRequest(messageData) }
- .asPromise()
- .receive(on: stateQueue)
- .onFailure { error in
+ stateQueue.async {
+ let request: TunnelIPC.Request
+ do {
+ request = try TunnelIPC.Coding.decodeRequest(messageData)
+ } catch {
self.providerLogger.error(chainedError: AnyChainedError(error), message: "Failed to decode the IPC request.")
+ completionHandler?(nil)
+ return
}
- .success()
- .mapThen(defaultValue: nil) { request in
- switch request {
- case .tunnelConnectionInfo:
- return Result { try TunnelIPC.Coding.encodeResponse(self.connectionInfo) }
- .asPromise()
- .onFailure { error in
- self.providerLogger.error(chainedError: AnyChainedError(error), message: "Failed to encode tunnel connection info IPC response.")
- }
- .success()
- case .reloadTunnelSettings:
- self.reasserting = true
- self.connectionInfo = self.pickRelay()?.tunnelConnectionInfo
- self.reasserting = false
+ var response: Data?
- return .resolved(nil)
+ switch request {
+ case .getTunnelStatus:
+ do {
+ response = try TunnelIPC.Coding.encodeResponse(self.tunnelStatus)
+ } catch {
+ self.providerLogger.error(chainedError: AnyChainedError(error), message: "Failed to encode tunnel status IPC response.")
}
+
+ case .reloadTunnelSettings:
+ self.reasserting = true
+ self.tunnelStatus.tunnelRelay = self.pickRelay()?.packetTunnelRelay
+ self.reasserting = false
}
- .observe { completion in
- completionHandler?(completion.unwrappedValue ?? nil)
- }
+
+ completionHandler?(response)
+ }
}
private func pickRelay() -> RelaySelectorResult? {
@@ -93,13 +93,13 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
constraints: entry.tunnelSettings.relayConstraints
)
case .failure(let error):
- self.providerLogger.error(chainedError: error, message: "Failed to load tunnel settings when picking relay")
+ self.providerLogger.error(chainedError: error, message: "Failed to load tunnel settings when picking relay.")
return nil
}
case .failure(let error):
- self.providerLogger.error(chainedError: error, message: "Failed to read relays when picking relay")
+ self.providerLogger.error(chainedError: error, message: "Failed to read relays when picking relay.")
return nil
}
}
diff --git a/ios/MullvadVPN/Tunnel.swift b/ios/MullvadVPN/Tunnel.swift
new file mode 100644
index 0000000000..058b6b2a26
--- /dev/null
+++ b/ios/MullvadVPN/Tunnel.swift
@@ -0,0 +1,198 @@
+//
+// Tunnel.swift
+// MullvadVPN
+//
+// Created by pronebird on 25/02/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import NetworkExtension
+
+// Switch to stabs on simulator
+#if targetEnvironment(simulator)
+typealias TunnelProviderManagerType = SimulatorTunnelProviderManager
+#else
+typealias TunnelProviderManagerType = NETunnelProviderManager
+#endif
+
+protocol TunnelStatusObserver {
+ func tunnel(_ tunnel: Tunnel, didReceiveStatus status: NEVPNStatus)
+}
+
+/// Tunnel wrapper class.
+class Tunnel {
+ /// Tunnel provider manager.
+ fileprivate let tunnelProvider: TunnelProviderManagerType
+
+ /// Tunnel start date.
+ ///
+ /// It's set to `distantPast` when the VPN connection was established prior to being observed
+ /// by the class.
+ var startDate: Date? {
+ lock.lock()
+ defer { lock.unlock() }
+
+ return _startDate
+ }
+
+ /// Tunnel connection status.
+ var status: NEVPNStatus {
+ return tunnelProvider.connection.status
+ }
+
+ /// Whether on-demand VPN is enabled.
+ var isOnDemandEnabled: Bool {
+ get {
+ return tunnelProvider.isOnDemandEnabled
+ }
+ set {
+ tunnelProvider.isOnDemandEnabled = newValue
+ }
+ }
+
+ private let lock = NSLock()
+ private var observerList = ObserverList<TunnelStatusObserver>()
+
+ private var _startDate: Date?
+
+ init(tunnelProvider: TunnelProviderManagerType) {
+ self.tunnelProvider = tunnelProvider
+
+ NotificationCenter.default.addObserver(
+ self, selector: #selector(handleVPNStatusChangeNotification(_:)),
+ name: .NEVPNStatusDidChange,
+ object: tunnelProvider.connection
+ )
+
+ handleVPNStatus(tunnelProvider.connection.status)
+ }
+
+ func start(options: [String: NSObject]?) throws {
+ try tunnelProvider.connection.startVPNTunnel(options: options)
+ }
+
+ func stop() {
+ tunnelProvider.connection.stopVPNTunnel()
+ }
+
+ func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws {
+ let session = tunnelProvider.connection as! VPNTunnelProviderSessionProtocol
+
+ try session.sendProviderMessage(messageData, responseHandler: responseHandler)
+ }
+
+ func saveToPreferences(_ completion: @escaping (Error?) -> Void) {
+ tunnelProvider.saveToPreferences(completionHandler: completion)
+ }
+
+ func removeFromPreferences(completion: @escaping (Error?) -> Void) {
+ tunnelProvider.removeFromPreferences(completionHandler: completion)
+ }
+
+ func addBlockObserver(queue: DispatchQueue? = nil, handler: @escaping (Tunnel, NEVPNStatus) -> Void) -> StatusBlockObserver {
+ let observer = StatusBlockObserver(tunnel: self, queue: queue, handler: handler)
+
+ addObserver(observer)
+
+ return observer
+ }
+
+ func addObserver(_ observer: TunnelStatusObserver) {
+ observerList.append(observer)
+ }
+
+ func removeObserver(_ observer: TunnelStatusObserver) {
+ observerList.remove(observer)
+ }
+
+ @objc private func handleVPNStatusChangeNotification(_ notification: Notification) {
+ guard let connection = notification.object as? VPNConnectionProtocol else { return }
+
+ let newStatus = connection.status
+
+ handleVPNStatus(newStatus)
+
+ observerList.forEach { observer in
+ observer.tunnel(self, didReceiveStatus: newStatus)
+ }
+ }
+
+ private func handleVPNStatus(_ status: NEVPNStatus) {
+ switch status {
+ case .connecting:
+ lock.lock()
+ _startDate = Date()
+ lock.unlock()
+
+ case .connected, .reasserting:
+ lock.lock()
+ if _startDate == nil {
+ _startDate = .distantPast
+ }
+ lock.unlock()
+
+ case .disconnecting:
+ break
+
+ case .disconnected, .invalid:
+ lock.lock()
+ _startDate = nil
+ lock.unlock()
+
+ @unknown default:
+ break
+ }
+ }
+}
+
+extension Tunnel: Equatable {
+ static func == (lhs: Tunnel, rhs: Tunnel) -> Bool {
+ return lhs.tunnelProvider == rhs.tunnelProvider
+ }
+}
+
+extension Tunnel {
+
+ final class StatusBlockObserver: TunnelStatusObserver {
+ typealias Handler = (Tunnel, NEVPNStatus) -> Void
+
+ private weak var tunnel: Tunnel?
+ private let queue: DispatchQueue?
+ private let lock = NSLock()
+ private var handler: Handler?
+
+ fileprivate init(tunnel: Tunnel, queue: DispatchQueue?, handler: @escaping Handler) {
+ self.tunnel = tunnel
+ self.queue = queue
+ self.handler = handler
+ }
+
+ func invalidate() {
+ lock.lock()
+ handler = nil
+ lock.unlock()
+
+ tunnel?.removeObserver(self)
+ }
+
+ func tunnel(_ tunnel: Tunnel, didReceiveStatus status: NEVPNStatus) {
+ if let queue = queue {
+ queue.async {
+ self.invokeHandler(tunnel: tunnel, status: status)
+ }
+ } else {
+ invokeHandler(tunnel: tunnel, status: status)
+ }
+ }
+
+ private func invokeHandler(tunnel: Tunnel, status: NEVPNStatus) {
+ lock.lock()
+ let block = handler
+ lock.unlock()
+
+ block?(tunnel, status)
+ }
+ }
+
+}
diff --git a/ios/MullvadVPN/TunnelIPC/PacketTunnelStatus.swift b/ios/MullvadVPN/TunnelIPC/PacketTunnelStatus.swift
new file mode 100644
index 0000000000..3dc17a587d
--- /dev/null
+++ b/ios/MullvadVPN/TunnelIPC/PacketTunnelStatus.swift
@@ -0,0 +1,36 @@
+//
+// PacketTunnelStatus.swift
+// PacketTunnelStatus
+//
+// Created by pronebird on 27/07/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+/// A struct that holds packet tunnel process status.
+struct PacketTunnelStatus: Codable, Equatable {
+ /// Flag indicating whether network is reachable.
+ var isNetworkReachable: Bool
+
+ /// When the packet tunnel started connecting.
+ var connectingDate: Date?
+
+ /// Current relay.
+ var tunnelRelay: PacketTunnelRelay?
+}
+
+/// A struct that holds the relay endpoints and location.
+struct PacketTunnelRelay: Codable, Equatable {
+ /// IPv4 relay endpoint.
+ let ipv4Relay: IPv4Endpoint
+
+ /// IPv6 relay endpoint.
+ let ipv6Relay: IPv6Endpoint?
+
+ /// Relay hostname.
+ let hostname: String
+
+ /// Relay location.
+ let location: Location
+}
diff --git a/ios/MullvadVPN/TunnelIPC/TunnelConnectionInfo.swift b/ios/MullvadVPN/TunnelIPC/TunnelConnectionInfo.swift
deleted file mode 100644
index 9558864e6a..0000000000
--- a/ios/MullvadVPN/TunnelIPC/TunnelConnectionInfo.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-// TunnelConnectionInfo.swift
-// TunnelConnectionInfo
-//
-// Created by pronebird on 27/07/2021.
-// Copyright © 2021 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-/// A struct that holds basic information regarding the tunnel connection.
-struct TunnelConnectionInfo: Codable, Equatable {
- let ipv4Relay: IPv4Endpoint
- let ipv6Relay: IPv6Endpoint?
- let hostname: String
- let location: Location
-}
diff --git a/ios/MullvadVPN/TunnelIPC/TunnelIPCRequest.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCRequest.swift
index 7bfa4de803..f331e7d1f5 100644
--- a/ios/MullvadVPN/TunnelIPC/TunnelIPCRequest.swift
+++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCRequest.swift
@@ -14,15 +14,15 @@ extension TunnelIPC {
/// Request the tunnel to reload settings.
case reloadTunnelSettings
- /// Request the tunnel connection info.
- case tunnelConnectionInfo
+ /// Request the tunnel status.
+ case getTunnelStatus
var description: String {
switch self {
case .reloadTunnelSettings:
return "reloadTunnelSettings"
- case .tunnelConnectionInfo:
- return "tunnelConnectionInfo"
+ case .getTunnelStatus:
+ return "getTunnelStatus"
}
}
diff --git a/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift
index 0b74f6c737..c634b679e6 100644
--- a/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift
+++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift
@@ -12,9 +12,9 @@ import NetworkExtension
extension TunnelIPC {
struct RequestOptions {
- /// Wait until the tunnel transitioned from reasserting to connected state before sending
- /// the request.
- var waitIfReasserting: Bool
+ /// Delay for sending IPC requests to the tunnel when in connecting state.
+ /// Used to workaround a bug when talking to the tunnel too early may cause it to freeze.
+ static let connectingStateWaitDelay: TimeInterval = 5
/// Timeout interval in seconds.
var timeout: TimeInterval = 5
@@ -25,30 +25,30 @@ extension TunnelIPC {
typealias CompletionHandler = (OperationCompletion<Output, TunnelIPC.Error>) -> Void
private let queue: DispatchQueue
- private let notificationQueue: OperationQueue
- private let connection: VPNConnectionProtocol
+ private let tunnel: Tunnel
private let request: TunnelIPC.Request
private let options: RequestOptions
private let decoderHandler: DecoderHandler
private var completionHandler: CompletionHandler?
- private var statusObserver: NSObjectProtocol?
- private var timeoutTimer: DispatchSourceTimer?
+ private var statusObserver: Tunnel.StatusBlockObserver?
+ private var timeoutWork: DispatchWorkItem?
+ private var waitForConnectingStateWork: DispatchWorkItem?
+
+ private var requestSent = false
init(queue: DispatchQueue,
- connection: VPNConnectionProtocol,
+ tunnel: Tunnel,
request: TunnelIPC.Request,
options: TunnelIPC.RequestOptions,
decoderHandler: @escaping DecoderHandler,
completionHandler: @escaping CompletionHandler)
{
self.queue = queue
- self.notificationQueue = OperationQueue()
- self.notificationQueue.underlyingQueue = queue
- self.connection = connection
+ self.tunnel = tunnel
self.request = request
self.options = options
@@ -78,47 +78,39 @@ extension TunnelIPC {
return
}
- startTimeoutTimer()
-
- statusObserver = NotificationCenter.default.addObserver(
- forName: .NEVPNStatusDidChange,
- object: connection,
- queue: notificationQueue) { [weak self] notification in
- guard let self = self else { return }
- guard let connection = notification.object as? VPNConnectionProtocol else { return }
+ setTimeoutTimer(connectingStateWaitDelay: 0)
- self.handleVPNStatus(connection.status)
- }
+ statusObserver = tunnel.addBlockObserver(queue: queue) { [weak self] tunnel, status in
+ self?.handleVPNStatus(status)
+ }
- handleVPNStatus(connection.status)
+ handleVPNStatus(tunnel.status)
}
private func removeVPNStatusObserver() {
- if let statusObserver = statusObserver {
- NotificationCenter.default.removeObserver(statusObserver)
- self.statusObserver = nil
- }
+ statusObserver?.invalidate()
+ statusObserver = nil
}
- private func startTimeoutTimer() {
- let timer = DispatchSource.makeTimerSource(queue: queue)
- timer.setEventHandler { [weak self] in
+ private func setTimeoutTimer(connectingStateWaitDelay: TimeInterval) {
+ let workItem = DispatchWorkItem { [weak self] in
self?.completeOperation(completion: .failure(.send(.timeout)))
}
- timer.schedule(wallDeadline: .now() + options.timeout)
- timer.activate()
+ // Cancel pending timeout work.
+ timeoutWork?.cancel()
- timeoutTimer = timer
- }
+ // Assign new timeout work.
+ timeoutWork = workItem
- private func stopTimeoutTimer() {
- timeoutTimer?.cancel()
- timeoutTimer = nil
+ // Schedule timeout work.
+ let deadline: DispatchWallTime = .now() + options.timeout + connectingStateWaitDelay
+
+ queue.asyncAfter(wallDeadline: deadline, execute: workItem)
}
private func handleVPNStatus(_ status: NEVPNStatus) {
- guard !isCancelled else {
+ guard !isCancelled && !requestSent else {
return
}
@@ -127,14 +119,12 @@ extension TunnelIPC {
sendRequest()
case .connecting:
- // Sending IPC message while in connecting state may cause the tunnel process to
- // freeze for no apparent reason.
- break
+ waitForConnectingState { [weak self] in
+ self?.sendRequest()
+ }
case .reasserting:
- if !options.waitIfReasserting {
- sendRequest()
- }
+ sendRequest()
case .invalid, .disconnecting, .disconnected:
completeOperation(completion: .failure(.send(.tunnelDown(status))))
@@ -144,11 +134,51 @@ extension TunnelIPC {
}
}
+ private func waitForConnectingState(block: @escaping () -> Void) {
+ // Compute amount of time elapsed since the tunnel was launched.
+ let timeElapsed: TimeInterval
+ if let startDate = tunnel.startDate {
+ timeElapsed = Date().timeIntervalSince(startDate)
+ } else {
+ timeElapsed = 0
+ }
+
+ // Cancel pending work.
+ waitForConnectingStateWork?.cancel()
+ waitForConnectingStateWork = nil
+
+ // Execute right away if enough time passed since the tunnel was launched.
+ guard timeElapsed < RequestOptions.connectingStateWaitDelay else {
+ block()
+ return
+ }
+
+ let waitDelay = RequestOptions.connectingStateWaitDelay - timeElapsed
+ let workItem = DispatchWorkItem(block: block)
+
+ // Assign new work.
+ waitForConnectingStateWork = workItem
+
+ // Reschedule the timeout work.
+ setTimeoutTimer(connectingStateWaitDelay: waitDelay)
+
+ // Schedule delayed work.
+ let deadline: DispatchWallTime = .now() + waitDelay
+
+ queue.asyncAfter(wallDeadline: deadline, execute: workItem)
+ }
+
private func sendRequest() {
- let session = connection as! VPNTunnelProviderSessionProtocol
+ // Mark request sent.
+ requestSent = true
+ // Release status observer.
removeVPNStatusObserver()
+ // Cancel pending delayed work.
+ waitForConnectingStateWork?.cancel()
+
+ // Encode request.
let messageData: Data
do {
messageData = try TunnelIPC.Coding.encodeRequest(request)
@@ -157,8 +187,9 @@ extension TunnelIPC {
return
}
+ // Send IPC message.
do {
- try session.sendProviderMessage(messageData) { [weak self] responseData in
+ try tunnel.sendProviderMessage(messageData) { [weak self] responseData in
guard let self = self else { return }
self.queue.async {
@@ -173,12 +204,18 @@ extension TunnelIPC {
}
private func completeOperation(completion: OperationCompletion<Output, TunnelIPC.Error>) {
+ // Release status observer.
removeVPNStatusObserver()
- stopTimeoutTimer()
+ // Cancel pending work.
+ timeoutWork?.cancel()
+ waitForConnectingStateWork?.cancel()
+
+ // Call completion handler.
completionHandler?(completion)
completionHandler = nil
+ // Finish operation.
finish()
}
}
@@ -187,7 +224,7 @@ extension TunnelIPC {
extension TunnelIPC.RequestOperation where Output: Codable {
convenience init(
queue: DispatchQueue,
- connection: VPNConnectionProtocol,
+ tunnel: Tunnel,
request: TunnelIPC.Request,
options: TunnelIPC.RequestOptions,
completionHandler: @escaping CompletionHandler
@@ -195,7 +232,7 @@ extension TunnelIPC.RequestOperation where Output: Codable {
{
self.init(
queue: queue,
- connection: connection,
+ tunnel: tunnel,
request: request,
options: options,
decoderHandler: { data in
@@ -215,14 +252,14 @@ extension TunnelIPC.RequestOperation where Output: Codable {
extension TunnelIPC.RequestOperation where Output == Void {
convenience init(
queue: DispatchQueue,
- connection: VPNConnectionProtocol,
+ tunnel: Tunnel,
request: TunnelIPC.Request,
options: TunnelIPC.RequestOptions,
completionHandler: @escaping CompletionHandler
) {
self.init(
queue: queue,
- connection: connection,
+ tunnel: tunnel,
request: request,
options: options,
decoderHandler: { _ in .success(()) },
diff --git a/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift
index 4b12b15db4..ce430b5b81 100644
--- a/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift
+++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift
@@ -13,20 +13,20 @@ extension TunnelIPC {
/// Wrapper class around `NETunnelProviderSession` that provides convenient interface for
/// interacting with the Packet Tunnel process.
final class Session {
- private let connection: VPNConnectionProtocol
+ private let tunnel: Tunnel
private let queue = DispatchQueue(label: "TunnelIPC.SessionQueue")
private let operationQueue = OperationQueue()
- init(connection: VPNConnectionProtocol) {
- self.connection = connection
+ init(tunnel: Tunnel) {
+ self.tunnel = tunnel
}
func reloadTunnelSettings(completionHandler: @escaping (OperationCompletion<(), TunnelIPC.Error>) -> Void) -> Cancellable {
let operation = RequestOperation(
queue: queue,
- connection: connection,
+ tunnel: tunnel,
request: .reloadTunnelSettings,
- options: TunnelIPC.RequestOptions(waitIfReasserting: true),
+ options: TunnelIPC.RequestOptions(),
completionHandler: completionHandler
)
@@ -37,12 +37,12 @@ extension TunnelIPC {
}
}
- func getTunnelConnectionInfo(completionHandler: @escaping (OperationCompletion<TunnelConnectionInfo?, TunnelIPC.Error>) -> Void) -> Cancellable {
- let operation = RequestOperation<TunnelConnectionInfo?>(
+ func getTunnelStatus(completionHandler: @escaping (OperationCompletion<PacketTunnelStatus, TunnelIPC.Error>) -> Void) -> Cancellable {
+ let operation = RequestOperation<PacketTunnelStatus>(
queue: queue,
- connection: connection,
- request: .tunnelConnectionInfo,
- options: TunnelIPC.RequestOptions(waitIfReasserting: false),
+ tunnel: tunnel,
+ request: .getTunnelStatus,
+ options: TunnelIPC.RequestOptions(),
completionHandler: completionHandler
)
diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift
index 8cdb64bcab..fc393b7494 100644
--- a/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift
@@ -118,7 +118,7 @@ class LoadTunnelOperation: AsyncOperation {
let tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: keychainEntry.tunnelSettings)
state.tunnelInfo = tunnelInfo
- state.setTunnelProvider(tunnelProvider, shouldRefreshTunnelState: true)
+ state.setTunnel(Tunnel(tunnelProvider: tunnelProvider), shouldRefreshTunnelState: true)
completionHandler(.success(()))
diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
index 6292b831b7..bef06ac128 100644
--- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
@@ -43,31 +43,49 @@ class MapConnectionStatusOperation: AsyncOperation {
}
private func execute() {
- guard let tunnelProvider = state.tunnelProvider, !isCancelled else {
+ guard let tunnel = state.tunnel, !isCancelled else {
finish()
return
}
- let tunnelState = state.tunnelState
+ let tunnelState = state.tunnelStatus.state
switch connectionStatus {
case .connecting:
switch tunnelState {
case .connecting(.some(_)):
- logger.debug("Ignore repeating connecting state.")
+ break
default:
- state.tunnelState = .connecting(nil)
+ state.tunnelStatus.state = .connecting(nil)
+ }
+
+ let session = TunnelIPC.Session(tunnel: tunnel)
+
+ request = session.getTunnelStatus { [weak self] completion in
+ guard let self = self else { return }
+
+ self.queue.async {
+ if case .success(let packetTunnelStatus) = completion, !self.isCancelled {
+ self.state.tunnelStatus.update(from: packetTunnelStatus) { relay in
+ return .connecting(relay)
+ }
+ }
+
+ self.finish()
+ }
}
case .reasserting:
- let session = TunnelIPC.Session(connection: tunnelProvider.connection)
+ let session = TunnelIPC.Session(tunnel: tunnel)
- request = session.getTunnelConnectionInfo { [weak self] completion in
+ request = session.getTunnelStatus { [weak self] completion in
guard let self = self else { return }
self.queue.async {
- if case .success(.some(let connectionInfo)) = completion, !self.isCancelled {
- self.state.tunnelState = .reconnecting(connectionInfo)
+ if case .success(let packetTunnelStatus) = completion, !self.isCancelled {
+ self.state.tunnelStatus.update(from: packetTunnelStatus) { relay in
+ return relay.map { .reconnecting($0) }
+ }
}
self.finish()
@@ -77,14 +95,16 @@ class MapConnectionStatusOperation: AsyncOperation {
return
case .connected:
- let session = TunnelIPC.Session(connection: tunnelProvider.connection)
+ let session = TunnelIPC.Session(tunnel: tunnel)
- request = session.getTunnelConnectionInfo { [weak self] completion in
+ request = session.getTunnelStatus { [weak self] completion in
guard let self = self else { return }
self.queue.async {
- if case .success(.some(let connectionInfo)) = completion, !self.isCancelled {
- self.state.tunnelState = .connected(connectionInfo)
+ if case .success(let packetTunnelStatus) = completion, !self.isCancelled {
+ self.state.tunnelStatus.update(from: packetTunnelStatus) { relay in
+ return relay.map { .connected($0) }
+ }
}
self.finish()
@@ -101,13 +121,13 @@ class MapConnectionStatusOperation: AsyncOperation {
case .disconnecting(.reconnect):
logger.debug("Restart the tunnel on disconnect.")
- state.tunnelState = .pendingReconnect
+ state.tunnelStatus.reset(to: .pendingReconnect)
startTunnelHandler?()
startTunnelHandler = nil
default:
- state.tunnelState = .disconnected
+ state.tunnelStatus.reset(to: .disconnected)
}
case .disconnecting:
@@ -115,11 +135,11 @@ class MapConnectionStatusOperation: AsyncOperation {
case .disconnecting:
break
default:
- state.tunnelState = .disconnecting(.nothing)
+ state.tunnelStatus.reset(to: .disconnecting(.nothing))
}
case .invalid:
- state.tunnelState = .disconnected
+ state.tunnelStatus.reset(to: .disconnected)
@unknown default:
logger.debug("Unknown NEVPNStatus: \(connectionStatus.rawValue)")
diff --git a/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
index 3758c9e4e9..090a29f59f 100644
--- a/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
@@ -29,12 +29,12 @@ class ReloadTunnelOperation: AsyncOperation {
return
}
- guard let tunnelProvider = self.state.tunnelProvider else {
+ guard let tunnel = self.state.tunnel else {
self.completeOperation(completion: .failure(.unsetAccount))
return
}
- let session = TunnelIPC.Session(connection: tunnelProvider.connection)
+ let session = TunnelIPC.Session(tunnel: tunnel)
self.request = session.reloadTunnelSettings { [weak self] completion in
guard let self = self else { return }
diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
index f09ee90f68..162ebfff73 100644
--- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
@@ -165,7 +165,7 @@ class SetAccountOperation: AsyncOperation {
willDeleteVPNConfigurationHandler = nil
// Reset tunnel state to disconnected
- state.tunnelState = .disconnected
+ state.tunnelStatus.reset(to: .disconnected)
// Remove tunnel info
state.tunnelInfo = nil
@@ -182,13 +182,13 @@ class SetAccountOperation: AsyncOperation {
}
// Finish immediately if tunnel provider is not set.
- guard let tunnelProvider = state.tunnelProvider else {
+ guard let tunnel = state.tunnel else {
completionHandler()
return
}
// Remove VPN configuration
- tunnelProvider.removeFromPreferences { error in
+ tunnel.removeFromPreferences { error in
self.queue.async {
// Ignore error but log it
if let error = error {
@@ -198,7 +198,7 @@ class SetAccountOperation: AsyncOperation {
)
}
- self.state.setTunnelProvider(nil, shouldRefreshTunnelState: false)
+ self.state.setTunnel(nil, shouldRefreshTunnelState: false)
completionHandler()
}
diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
index 1037253da5..ac8223f963 100644
--- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
@@ -48,9 +48,9 @@ class StartTunnelOperation: AsyncOperation {
return
}
- switch self.state.tunnelState {
+ switch self.state.tunnelStatus.state {
case .disconnecting(.nothing):
- self.state.tunnelState = .disconnecting(.reconnect)
+ self.state.tunnelStatus.state = .disconnecting(.reconnect)
completionHandler(.success(()))
@@ -114,8 +114,8 @@ class StartTunnelOperation: AsyncOperation {
encodeErrorHandler = nil
- state.setTunnelProvider(tunnelProvider, shouldRefreshTunnelState: false)
- state.tunnelState = .connecting(selectorResult.tunnelConnectionInfo)
+ state.setTunnel(Tunnel(tunnelProvider: tunnelProvider), shouldRefreshTunnelState: false)
+ state.tunnelStatus.reset(to: .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 297805eb8e..377d946399 100644
--- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
@@ -38,27 +38,27 @@ class StopTunnelOperation: AsyncOperation {
return
}
- guard let tunnelProvider = state.tunnelProvider else {
+ guard let tunnel = state.tunnel else {
completionHandler(.failure(.unsetAccount))
return
}
- switch self.state.tunnelState {
+ switch self.state.tunnelStatus.state {
case .disconnecting(.reconnect):
- state.tunnelState = .disconnecting(.nothing)
+ state.tunnelStatus.state = .disconnecting(.nothing)
completionHandler(.success(()))
- case .connected, .connecting:
+ case .connected, .connecting, .reconnecting:
// Disable on-demand when stopping the tunnel to prevent it from coming back up
- tunnelProvider.isOnDemandEnabled = false
+ tunnel.isOnDemandEnabled = false
- tunnelProvider.saveToPreferences { error in
+ tunnel.saveToPreferences { error in
self.queue.async {
if let error = error {
completionHandler(.failure(.saveVPNConfiguration(error)))
} else {
- tunnelProvider.connection.stopVPNTunnel()
+ tunnel.stop()
completionHandler(.success(()))
}
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 96cf744505..d08e273475 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -13,16 +13,32 @@ import UIKit
import Logging
import class WireGuardKitTypes.PublicKey
-/// A class that provides a convenient interface for VPN tunnels configuration, manipulation and
-/// monitoring.
-class TunnelManager: TunnelManagerStateDelegate
-{
- /// Private key rotation interval (in seconds)
- private static let privateKeyRotationInterval: TimeInterval = 60 * 60 * 24 * 4
+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
+
+ /// Poll interval used when connecting date is unknown (in seconds).
+ static let tunnelStatusQuickPollInterval: TimeInterval = 3
+
+ /// 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 retry interval (in seconds)
- private static let privateKeyRotationFailureRetryInterval: TimeInterval = 60 * 15
+ /// 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
+}
+
+/// A class that provides a convenient interface for VPN tunnels configuration, manipulation and
+/// monitoring.
+final class TunnelManager: TunnelManagerStateDelegate {
/// Operation categories
private enum OperationCategory {
static let manageTunnelProvider = "TunnelManager.manageTunnelProvider"
@@ -43,17 +59,25 @@ class TunnelManager: TunnelManagerStateDelegate
private let operationQueue = OperationQueue()
private let exclusivityController = ExclusivityController()
+ private var statusObserver: Tunnel.StatusBlockObserver?
private var lastMapConnectionStatusOperation: Operation?
private let observerList = ObserverList<TunnelObserver>()
private let state: TunnelManager.State
+ private var privateKeyRotationTimer: DispatchSourceTimer?
+ private var isRunningPeriodicPrivateKeyRotation = false
+
+ private var tunnelStatusPollTimer: DispatchSourceTimer?
+ private var isPolling = false
+ private var lastConnectingDate: Date?
+
var tunnelInfo: TunnelInfo? {
return state.tunnelInfo
}
var tunnelState: TunnelState {
- return state.tunnelState
+ return state.tunnelStatus.state
}
private init(restClient: REST.Client) {
@@ -71,14 +95,11 @@ class TunnelManager: TunnelManagerStateDelegate
// MARK: - Periodic private key rotation
- private var privateKeyRotationTimer: DispatchSourceTimer?
- private var isRunningPeriodicPrivateKeyRotation = false
-
func startPeriodicPrivateKeyRotation() {
stateQueue.async {
guard !self.isRunningPeriodicPrivateKeyRotation else { return }
- self.logger.debug("Start periodic private key rotation")
+ self.logger.debug("Start periodic private key rotation.")
self.isRunningPeriodicPrivateKeyRotation = true
@@ -90,7 +111,7 @@ class TunnelManager: TunnelManagerStateDelegate
stateQueue.async {
guard self.isRunningPeriodicPrivateKeyRotation else { return }
- self.logger.debug("Stop periodic private key rotation")
+ self.logger.debug("Stop periodic private key rotation.")
self.isRunningPeriodicPrivateKeyRotation = false
@@ -106,7 +127,7 @@ class TunnelManager: TunnelManagerStateDelegate
if let tunnelInfo = self.state.tunnelInfo {
let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate
- let scheduleDate = Date(timeInterval: Self.privateKeyRotationInterval, since: creationDate)
+ let scheduleDate = Date(timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, since: creationDate)
schedulePrivateKeyRotationTimer(scheduleDate)
} else {
@@ -258,6 +279,11 @@ class TunnelManager: TunnelManagerStateDelegate
self.logger.error(chainedError: error, message: "Failed to reconnect the tunnel.")
}
+ // Refresh tunnel status since reasserting may not be lowered until the tunnel is fully
+ // connected.
+ self.logger.debug("Refresh tunnel status due to reconnect.")
+ self.refreshTunnelStatus()
+
DispatchQueue.main.async {
completionHandler?()
}
@@ -357,7 +383,7 @@ class TunnelManager: TunnelManagerStateDelegate
queue: stateQueue,
state: state,
restClient: restClient,
- rotationInterval: Self.privateKeyRotationInterval) { [weak self] completion in
+ rotationInterval: TunnelManagerConfiguration.privateKeyRotationInterval) { [weak self] completion in
guard let self = self else { return }
dispatchPrecondition(condition: .onQueue(self.stateQueue))
@@ -449,63 +475,76 @@ class TunnelManager: TunnelManagerStateDelegate
}
}
- func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelState newTunnelState: TunnelState) {
- logger.info("Set tunnel state: \(newTunnelState)")
+ func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelStatus newTunnelStatus: TunnelStatus) {
+ logger.info("Status: \(newTunnelStatus).")
+
+ switch newTunnelStatus.state {
+ 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.connectingDate)
+
+ case .pendingReconnect, .connected, .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: newTunnelState)
+ observer.tunnelManager(self, didUpdateTunnelState: newTunnelStatus.state)
}
}
}
- func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelProvider: TunnelProviderManagerType?, shouldRefreshTunnelState: Bool) {
+ func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelObject: Tunnel?, shouldRefreshTunnelState: Bool) {
dispatchPrecondition(condition: .onQueue(stateQueue))
// Register for tunnel connection status changes
- if let newTunnelProvider = newTunnelProvider {
- subscribeVPNStatusObserver(for: newTunnelProvider)
+ if let newTunnelObject = newTunnelObject {
+ subscribeVPNStatusObserver(tunnel: newTunnelObject)
} else {
unsubscribeVPNStatusObserver()
}
// Update the existing state
if shouldRefreshTunnelState {
- updateTunnelState()
+ logger.debug("Refresh tunnel status for new tunnel.")
+ refreshTunnelStatus()
}
}
// MARK: - Private methods
- private func subscribeVPNStatusObserver(for tunnelProvider: TunnelProviderManagerType) {
+ private func subscribeVPNStatusObserver(tunnel: Tunnel) {
unsubscribeVPNStatusObserver()
- NotificationCenter.default.addObserver(
- self, selector: #selector(didReceiveVPNStatusChange(_:)),
- name: .NEVPNStatusDidChange,
- object: tunnelProvider.connection
- )
+ statusObserver = tunnel.addBlockObserver(queue: stateQueue) { [weak self] tunnel, status in
+ guard let self = self else { return }
+
+ self.logger.debug("VPN connection status changed to \(status).")
+ self.updateTunnelStatus(status)
+ }
}
private func unsubscribeVPNStatusObserver() {
- NotificationCenter.default.removeObserver(self, name: .NEVPNStatusDidChange, object: nil)
+ statusObserver?.invalidate()
+ statusObserver = nil
}
- @objc private func didReceiveVPNStatusChange(_ notification: Notification) {
- stateQueue.async {
- self.updateTunnelState()
+ private func refreshTunnelStatus() {
+ dispatchPrecondition(condition: .onQueue(stateQueue))
+
+ if let connectionStatus = self.state.tunnel?.status {
+ updateTunnelStatus(connectionStatus)
}
}
- /// Update `TunnelState` from `NEVPNStatus`.
- /// Collects the `TunnelConnectionInfo` from the tunnel via IPC if needed before assigning the `tunnelState`
- private func updateTunnelState() {
+ /// Update `TunnelStatus` from `NEVPNStatus`.
+ /// Collects the `PacketTunnelStatus` from the tunnel via IPC if needed before assigning
+ /// the `tunnelStatus`.
+ private func updateTunnelStatus(_ connectionStatus: NEVPNStatus) {
dispatchPrecondition(condition: .onQueue(stateQueue))
- guard let connectionStatus = self.state.tunnelProvider?.connection.status else { return }
-
- logger.debug("VPN status changed to \(connectionStatus)")
-
let operation = MapConnectionStatusOperation(queue: stateQueue, state: state, connectionStatus: connectionStatus) { [weak self] in
guard let self = self else { return }
@@ -525,8 +564,8 @@ class TunnelManager: TunnelManagerStateDelegate
@objc private func applicationDidBecomeActive() {
stateQueue.async {
- // Refresh tunnel state when application becomes active.
- self.updateTunnelState()
+ self.logger.debug("Refresh tunnel status due to application becoming active.")
+ self.refreshTunnelStatus()
}
}
@@ -598,6 +637,79 @@ class TunnelManager: TunnelManagerStateDelegate
operationQueue.addOperation(operation)
}
+ // 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)
+
+ // 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).")
+
+ let timer = DispatchSource.makeTimerSource(queue: stateQueue)
+ timer.setEventHandler { [weak self] in
+ guard let self = self else { return }
+
+ self.logger.debug("Refresh tunnel status (poll).")
+ self.refreshTunnelStatus()
+ }
+
+ timer.schedule(
+ wallDeadline: .now() + fireDate.timeIntervalSinceNow,
+ repeating: repeating
+ )
+
+ timer.resume()
+
+ tunnelStatusPollTimer?.cancel()
+ tunnelStatusPollTimer = timer
+ }
+
+ private func cancelPollingTunnelStatus() {
+ guard isPolling else { return }
+
+ logger.debug("Cancel tunnel status polling.")
+
+ tunnelStatusPollTimer?.cancel()
+ tunnelStatusPollTimer = nil
+ lastConnectingDate = nil
+ isPolling = false
+ }
+
}
extension TunnelManager {
@@ -644,7 +756,7 @@ extension TunnelManager {
func scheduleBackgroundTask() -> Result<(), TunnelManager.Error> {
if let tunnelInfo = self.state.tunnelInfo {
let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate
- let beginDate = Date(timeInterval: Self.privateKeyRotationInterval, since: creationDate)
+ let beginDate = Date(timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, since: creationDate)
return submitBackgroundTask(at: beginDate)
} else {
@@ -710,17 +822,17 @@ extension TunnelManager {
} else {
logger.debug("Private key rotation was cancelled")
- return Date(timeIntervalSinceNow: Self.privateKeyRotationFailureRetryInterval)
+ return Date(timeIntervalSinceNow: TunnelManagerConfiguration.privateKeyRotationFailureRetryInterval)
}
}
fileprivate func nextScheduleDate(_ result: KeyRotationResult) -> Date {
switch result {
case .finished:
- return Date(timeIntervalSinceNow: Self.privateKeyRotationInterval)
+ return Date(timeIntervalSinceNow: TunnelManagerConfiguration.privateKeyRotationInterval)
case .throttled(let lastKeyCreationDate):
- return Date(timeInterval: Self.privateKeyRotationInterval, since: lastKeyCreationDate)
+ return Date(timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, since: lastKeyCreationDate)
}
}
@@ -735,7 +847,7 @@ extension TunnelManager {
return nil
default:
- return Date(timeIntervalSinceNow: Self.privateKeyRotationFailureRetryInterval)
+ return Date(timeIntervalSinceNow: TunnelManagerConfiguration.privateKeyRotationFailureRetryInterval)
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift
index f8ebaf86b6..3802390376 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift
@@ -9,17 +9,10 @@
import Foundation
import NetworkExtension
-// Switch to stabs on simulator
-#if targetEnvironment(simulator)
-typealias TunnelProviderManagerType = SimulatorTunnelProviderManager
-#else
-typealias TunnelProviderManagerType = NETunnelProviderManager
-#endif
-
protocol TunnelManagerStateDelegate: AnyObject {
func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelInfo newTunnelInfo: TunnelInfo?)
- func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelState newTunnelState: TunnelState)
- func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelProvider: TunnelProviderManagerType?, shouldRefreshTunnelState: Bool)
+ func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelStatus newTunnelStatus: TunnelStatus)
+ func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelObject: Tunnel?, shouldRefreshTunnelState: Bool)
}
extension TunnelManager {
@@ -31,8 +24,12 @@ extension TunnelManager {
private let queueMarkerKey = DispatchSpecificKey<Bool>()
private var _tunnelInfo: TunnelInfo?
- private var _tunnelProvider: TunnelProviderManagerType?
- private var _tunnelState: TunnelState = .disconnected
+ private var _tunnelObject: Tunnel?
+ private var _tunnelStatus = TunnelStatus(
+ isNetworkReachable: false,
+ connectingDate: nil,
+ state: .disconnected
+ )
var tunnelInfo: TunnelInfo? {
get {
@@ -51,24 +48,24 @@ extension TunnelManager {
}
}
- var tunnelProvider: TunnelProviderManagerType? {
+ var tunnel: Tunnel? {
return performBlock {
- return _tunnelProvider
+ return _tunnelObject
}
}
- var tunnelState: TunnelState {
+ var tunnelStatus: TunnelStatus {
get {
return performBlock {
- return _tunnelState
+ return _tunnelStatus
}
}
set {
performBlock {
- if _tunnelState != newValue {
- _tunnelState = newValue
+ if _tunnelStatus != newValue {
+ _tunnelStatus = newValue
- delegate?.tunnelManagerState(self, didChangeTunnelState: newValue)
+ delegate?.tunnelManagerState(self, didChangeTunnelStatus: newValue)
}
}
}
@@ -84,12 +81,12 @@ extension TunnelManager {
queue.setSpecific(key: queueMarkerKey, value: nil)
}
- func setTunnelProvider(_ newTunnelProvider: TunnelProviderManagerType?, shouldRefreshTunnelState: Bool) {
+ func setTunnel(_ newTunnelObject: Tunnel?, shouldRefreshTunnelState: Bool) {
performBlock {
- if _tunnelProvider != newTunnelProvider {
- _tunnelProvider = newTunnelProvider
+ if _tunnelObject != newTunnelObject {
+ _tunnelObject = newTunnelObject
- delegate?.tunnelManagerState(self, didChangeTunnelProvider: newTunnelProvider, shouldRefreshTunnelState: shouldRefreshTunnelState)
+ delegate?.tunnelManagerState(self, didChangeTunnelProvider: newTunnelObject, shouldRefreshTunnelState: shouldRefreshTunnelState)
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift
index b016144dd3..bec15a9044 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelState.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift
@@ -8,16 +8,61 @@
import Foundation
-/// A enum that describes the tunnel state
+/// A struct describing the tunnel status.
+struct TunnelStatus: Equatable, CustomStringConvertible {
+ /// Whether netowork is reachable.
+ var isNetworkReachable: Bool
+
+ /// When the packet tunnel started connecting.
+ var connectingDate: Date?
+
+ /// Tunnel state.
+ var state: TunnelState
+
+ var description: String {
+ var s = "\(state), network "
+
+ if isNetworkReachable {
+ s += "reachable"
+ } else {
+ s += "unreachable"
+ }
+
+ if let connectingDate = 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?) {
+ isNetworkReachable = packetTunnelStatus.isNetworkReachable
+ connectingDate = packetTunnelStatus.connectingDate
+
+ 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) {
+ isNetworkReachable = true
+ connectingDate = nil
+ state = newState
+ }
+}
+
+/// An enum that describes the tunnel state.
enum TunnelState: Equatable, CustomStringConvertible {
/// Pending reconnect after disconnect.
case pendingReconnect
- /// Connecting the tunnel. Contains the pending action carried over from disconnected state.
- case connecting(TunnelConnectionInfo?)
+ /// Connecting the tunnel.
+ case connecting(_ relay: PacketTunnelRelay?)
/// Connected the tunnel
- case connected(TunnelConnectionInfo)
+ case connected(PacketTunnelRelay)
/// Disconnecting the tunnel
case disconnecting(ActionAfterDisconnect)
@@ -27,26 +72,26 @@ enum TunnelState: Equatable, CustomStringConvertible {
/// Reconnecting the tunnel. Normally this state appears in response to changing the
/// relay constraints and asking the running tunnel to reload the configuration.
- case reconnecting(TunnelConnectionInfo)
+ case reconnecting(_ relay: PacketTunnelRelay)
var description: String {
switch self {
case .pendingReconnect:
return "pending reconnect after disconnect"
- case .connecting(let connectionInfo):
- if let connectionInfo = connectionInfo {
- return "connecting to \(connectionInfo.hostname)"
+ case .connecting(let tunnelRelay):
+ if let tunnelRelay = tunnelRelay {
+ return "connecting to \(tunnelRelay.hostname)"
} else {
return "connecting, fetching relay"
}
- case .connected(let connectionInfo):
- return "connected to \(connectionInfo.hostname)"
+ case .connected(let tunnelRelay):
+ return "connected to \(tunnelRelay.hostname)"
case .disconnecting(let actionAfterDisconnect):
return "disconnecting and then \(actionAfterDisconnect)"
case .disconnected:
return "disconnected"
- case .reconnecting(let connectionInfo):
- return "reconnecting to \(connectionInfo.hostname)"
+ case .reconnecting(let tunnelRelay):
+ return "reconnecting to \(tunnelRelay.hostname)"
}
}
}
diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift
index 9c67b084a9..61bc7a9cd4 100644
--- a/ios/PacketTunnel/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider.swift
@@ -41,16 +41,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
/// Tunnel monitor.
private var tunnelMonitor: TunnelMonitor!
- /// Tunnel connection info.
- private var tunnelConnectionInfo: TunnelConnectionInfo? {
- didSet {
- if let tunnelConnectionInfo = tunnelConnectionInfo {
- self.providerLogger.debug("Set tunnel relay to \(tunnelConnectionInfo.hostname).")
- } else {
- self.providerLogger.debug("Unset tunnel relay.")
- }
- }
- }
+ /// Tunnel status.
+ private var tunnelStatus = PacketTunnelStatus(
+ isNetworkReachable: true,
+ connectingDate: nil,
+ tunnelRelay: nil
+ )
override init() {
let pid = ProcessInfo.processInfo.processIdentifier
@@ -85,7 +81,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
switch appSelectorResult {
case .some(let selectorResult):
- providerLogger.debug("Start the tunnel via app, connect to \(selectorResult.tunnelConnectionInfo.hostname).")
+ providerLogger.debug("Start the tunnel via app, connect to \(selectorResult.relay.hostname).")
case .none:
if tunnelOptions.isOnDemand() {
@@ -114,9 +110,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
return
}
- // Set tunnel connection info.
+ // Set tunnel status.
dispatchQueue.async {
- self.tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo
+ let tunnelRelay = tunnelConfiguration.selectorResult.packetTunnelRelay
+ self.tunnelStatus.tunnelRelay = tunnelRelay
+ self.providerLogger.debug("Set tunnel relay to \(tunnelRelay.hostname).")
}
// Start tunnel.
@@ -142,7 +140,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
// Start tunnel monitor.
let gatewayAddress = tunnelConfiguration.selectorResult.endpoint.ipv4Gateway
- self.tunnelMonitor.start(address: gatewayAddress)
+
+ self.startTunnelMonitor(gatewayAddress: gatewayAddress)
}
}
}
@@ -203,10 +202,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
completionHandler?(nil)
- case .tunnelConnectionInfo:
+ case .getTunnelStatus:
var response: Data?
do {
- response = try TunnelIPC.Coding.encodeResponse(self.tunnelConnectionInfo)
+ response = try TunnelIPC.Coding.encodeResponse(self.tunnelStatus)
} catch {
self.providerLogger.error(chainedError: AnyChainedError(error), message: "Failed to encode the app message response for \(request)")
}
@@ -232,6 +231,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
providerLogger.debug("Connection established.")
+ tunnelStatus.connectingDate = nil
+
startTunnelCompletionHandler?(nil)
startTunnelCompletionHandler = nil
@@ -272,8 +273,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
return
}
- // Set tunnel connection info.
- self.tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo
+ // Update tunnel status.
+ let tunnelRelay = tunnelConfiguration.selectorResult.packetTunnelRelay
+ tunnelStatus.tunnelRelay = tunnelRelay
+ providerLogger.debug("Set tunnel relay to \(tunnelRelay.hostname).")
// Update WireGuard configuration.
adapter.update(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in
@@ -285,6 +288,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
}
}
+ func tunnelMonitor(_ tunnelMonitor: TunnelMonitor, networkReachabilityStatusDidChange isNetworkReachable: Bool) {
+ tunnelStatus.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 {
+ tunnelStatus.connectingDate = startDate
+ }
+ }
+
// MARK: - Private
private func makeConfiguration(_ appSelectorResult: RelaySelectorResult? = nil) -> Result<PacketTunnelConfiguration, PacketTunnelProviderError> {
@@ -335,10 +348,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
return
}
- // Set tunnel connection info.
- let tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo
- let oldTunnelConnectionInfo = self.tunnelConnectionInfo
- self.tunnelConnectionInfo = tunnelConnectionInfo
+ // Copy old relay.
+ let oldTunnelRelay = tunnelStatus.tunnelRelay
+ let newTunnelRelay = tunnelConfiguration.selectorResult.packetTunnelRelay
+
+ // Update tunnel status.
+ tunnelStatus.tunnelRelay = newTunnelRelay
+ tunnelStatus.connectingDate = nil
+
+ providerLogger.debug("Set tunnel relay to \(newTunnelRelay.hostname).")
// 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
@@ -355,8 +373,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
// Call completion handler immediately on error to update adapter configuration.
if let error = error {
- // Revert to previously used tunnel connection info.
- self.tunnelConnectionInfo = oldTunnelConnectionInfo
+ // Revert to previously used tunnel relay.
+ self.tunnelStatus.tunnelRelay = oldTunnelRelay
+ self.providerLogger.debug("Reset tunnel relay to \(oldTunnelRelay?.hostname ?? "none").")
// Lower the reasserting flag.
if self.isConnected {
@@ -381,12 +400,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
// Restart tunnel monitor.
let gatewayAddress = tunnelConfiguration.selectorResult.endpoint.ipv4Gateway
- self.tunnelMonitor.start(address: gatewayAddress)
+
+ self.startTunnelMonitor(gatewayAddress: gatewayAddress)
}
}
}
}
+ private func startTunnelMonitor(gatewayAddress: IPv4Address) {
+ tunnelMonitor.start(address: gatewayAddress)
+
+ // Mark when the tunnel started monitoring connection.
+ tunnelStatus.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) -> Result<RelaySelectorResult, PacketTunnelProviderError> {