summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-02-08 13:41:25 +0100
committerAndrej Mihajlov <and@mullvad.net>2022-03-17 09:46:29 +0100
commit024715100b1170d2bd73a13c6ee6e03745f4d3da (patch)
treef6611bdd1651af841adc1e73a9d76ced2dda419e
parent078a0a8d436929ba0290f87e8d772668ff352add (diff)
downloadmullvadvpn-024715100b1170d2bd73a13c6ee6e03745f4d3da.tar.xz
mullvadvpn-024715100b1170d2bd73a13c6ee6e03745f4d3da.zip
Add tunnel monitor
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj14
-rw-r--r--ios/MullvadVPN/Logging/Logging.swift8
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift484
-rw-r--r--ios/PacketTunnel/Pinger.swift257
-rw-r--r--ios/PacketTunnel/TunnelMonitor.swift312
-rw-r--r--ios/PacketTunnel/TunnelMonitorConfiguration.swift23
6 files changed, 925 insertions, 173 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 1b6b6d4243..49ce76af67 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -91,6 +91,7 @@
582CFEE726945FC30072883A /* AppStoreSubscriptions.strings in Resources */ = {isa = PBXBuildFile; fileRef = 582CFEE526945FC30072883A /* AppStoreSubscriptions.strings */; };
582CFEEA269463B80072883A /* Settings.strings in Resources */ = {isa = PBXBuildFile; fileRef = 582CFEE8269463B80072883A /* Settings.strings */; };
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; };
+ 5838318B27C40A3900000571 /* Pinger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838318A27C40A3900000571 /* Pinger.swift */; };
583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DA21325FA4B5C00318683 /* LocationDataSource.swift */; };
5840250122B1124600E4CFEC /* IPAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */; };
5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */; };
@@ -160,6 +161,8 @@
5860392A26DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; };
5860392B26DCEE6300554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; };
5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; };
+ 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 */; };
586ADD4723FC13F400CE9E87 /* countries.geo.json in Resources */ = {isa = PBXBuildFile; fileRef = 586ADD4523FC13F400CE9E87 /* countries.geo.json */; };
@@ -320,6 +323,7 @@
58FB865E26EA284E00F188BC /* LogFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865D26EA284E00F188BC /* LogFormatting.swift */; };
58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865D26EA284E00F188BC /* LogFormatting.swift */; };
58FB866126EB678000F188BC /* TunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB866026EB677F00F188BC /* TunnelInfo.swift */; };
+ 58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */; };
58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */; };
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; };
58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */; };
@@ -420,6 +424,7 @@
582CFEE626945FC30072883A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppStoreSubscriptions.strings; sourceTree = "<group>"; };
582CFEE9269463B80072883A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Settings.strings; sourceTree = "<group>"; };
5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = "<group>"; };
+ 5838318A27C40A3900000571 /* Pinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pinger.swift; sourceTree = "<group>"; };
583DA21325FA4B5C00318683 /* LocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSource.swift; sourceTree = "<group>"; };
5840250022B1124600E4CFEC /* IPAddress+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IPAddress+Codable.swift"; sourceTree = "<group>"; };
5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; };
@@ -460,6 +465,7 @@
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>"; };
+ 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>"; };
@@ -601,6 +607,7 @@
58FB865926EA214400F188BC /* RelayCacheObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheObserver.swift; sourceTree = "<group>"; };
58FB865D26EA284E00F188BC /* LogFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogFormatting.swift; sourceTree = "<group>"; };
58FB866026EB677F00F188BC /* TunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelInfo.swift; sourceTree = "<group>"; };
+ 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitor.swift; sourceTree = "<group>"; };
58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreReceipt.swift; sourceTree = "<group>"; };
58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Formatting.swift"; sourceTree = "<group>"; };
58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInterfaceInteractionRestriction.swift; sourceTree = "<group>"; };
@@ -992,6 +999,9 @@
58CE5E7D224146470008646E /* Info.plist */,
58CE5E7E224146470008646E /* PacketTunnel.entitlements */,
58CE5E7B224146470008646E /* PacketTunnelProvider.swift */,
+ 5838318A27C40A3900000571 /* Pinger.swift */,
+ 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */,
+ 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */,
);
path = PacketTunnel;
sourceTree = "<group>";
@@ -1395,6 +1405,7 @@
58FB865A26EA214400F188BC /* RelayCacheObserver.swift in Sources */,
5806766C27048E3E00C858CB /* AnyOptional.swift in Sources */,
58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */,
+ 58655DCE27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */,
5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */,
58F2E14C276A61C000A79513 /* ReplaceKeyOperation.swift in Sources */,
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */,
@@ -1562,6 +1573,8 @@
58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */,
582AD44127BE6178002A6BFC /* CodingErrors+ChainedError.swift in Sources */,
5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */,
+ 58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */,
+ 5838318B27C40A3900000571 /* Pinger.swift in Sources */,
5820675C26E6576800655B05 /* RelayCache.swift in Sources */,
58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */,
585DA89A26B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */,
@@ -1578,6 +1591,7 @@
5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */,
5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */,
58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */,
+ 58655DCF27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */,
5815039E24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */,
585DA87826B024A900B8C587 /* CachedRelays.swift in Sources */,
584E96BD240FD4DA00D3334F /* Location.swift in Sources */,
diff --git a/ios/MullvadVPN/Logging/Logging.swift b/ios/MullvadVPN/Logging/Logging.swift
index b96d9dc08d..fe5b65b433 100644
--- a/ios/MullvadVPN/Logging/Logging.swift
+++ b/ios/MullvadVPN/Logging/Logging.swift
@@ -9,7 +9,7 @@
import Foundation
import Logging
-func initLoggingSystem(bundleIdentifier: String) {
+func initLoggingSystem(bundleIdentifier: String, metadata: Logger.Metadata? = nil) {
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier)!
let logsDirectoryURL = containerURL.appendingPathComponent("Logs", isDirectory: true)
let logFileName = "\(bundleIdentifier).log"
@@ -44,7 +44,11 @@ func initLoggingSystem(bundleIdentifier: String) {
if logHandlers.isEmpty {
return SwiftLogNoOpLogHandler()
} else {
- return MultiplexLogHandler(logHandlers)
+ var multiplex = MultiplexLogHandler(logHandlers)
+ if let metadata = metadata {
+ multiplex.metadata = metadata
+ }
+ return multiplex
}
}
diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift
index b9f645eb9d..9c67b084a9 100644
--- a/ios/PacketTunnel/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider.swift
@@ -12,146 +12,208 @@ import NetworkExtension
import Logging
import WireGuardKit
-class PacketTunnelProvider: NEPacketTunnelProvider {
+class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
- /// Tunnel provider logger
+ /// Tunnel provider logger.
private let providerLogger: Logger
- /// WireGuard adapter logger
+ /// WireGuard adapter logger.
private let tunnelLogger: Logger
- /// Internal queue
+ /// Internal queue.
private let dispatchQueue = DispatchQueue(label: "PacketTunnel", qos: .utility)
- /// WireGuard adapter
- private lazy var adapter: WireGuardAdapter = {
- return WireGuardAdapter(with: self, logHandler: { [weak self] (logLevel, message) in
- self?.dispatchQueue.async {
- self?.tunnelLogger.log(level: logLevel.loggerLevel, "\(message)")
- }
- })
- }()
+ /// WireGuard adapter.
+ private var adapter: WireGuardAdapter!
+
+ /// Raised once tunnel establishes connection in the very first time, before calling the system
+ /// completion handler passed into `startTunnel`.
+ private var isConnected = false
- /// Tunnel connection info
+ /// A system completion handler passed from startTunnel and saved for later use once the
+ /// connection is established.
+ private var startTunnelCompletionHandler: ((PacketTunnelProviderError?) -> Void)?
+
+ /// A completion handler passed during reassertion and saved for later use once the connection
+ /// is reestablished.
+ private var reassertTunnelCompletionHandler: ((PacketTunnelProviderError?) -> Void)?
+
+ /// 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)")
+ self.providerLogger.debug("Set tunnel relay to \(tunnelConnectionInfo.hostname).")
} else {
- self.providerLogger.debug("Unset tunnel relay")
+ self.providerLogger.debug("Unset tunnel relay.")
}
}
}
override init() {
- initLoggingSystem(bundleIdentifier: Bundle.main.bundleIdentifier!)
+ let pid = ProcessInfo.processInfo.processIdentifier
- var providerLogger = Logger(label: "PacketTunnelProvider")
- var tunnelLogger = Logger(label: "WireGuard")
+ var metadata = Logger.Metadata()
+ metadata["pid"] = .string("\(pid)")
- let pid = ProcessInfo.processInfo.processIdentifier
- providerLogger[metadataKey: "pid"] = .stringConvertible(pid)
- tunnelLogger[metadataKey: "pid"] = .stringConvertible(pid)
+ initLoggingSystem(bundleIdentifier: Bundle.main.bundleIdentifier!, metadata: metadata)
+
+ providerLogger = Logger(label: "PacketTunnelProvider")
+ tunnelLogger = Logger(label: "WireGuard")
+
+ super.init()
+
+ adapter = WireGuardAdapter(with: self, shouldHandleReasserting: false, logHandler: { [weak self] (logLevel, message) in
+ self?.dispatchQueue.async {
+ self?.tunnelLogger.log(level: logLevel.loggerLevel, "\(message)")
+ }
+ })
- self.providerLogger = providerLogger
- self.tunnelLogger = tunnelLogger
+ tunnelMonitor = TunnelMonitor(queue: dispatchQueue, adapter: adapter)
+ tunnelMonitor.delegate = self
}
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
let tunnelOptions = PacketTunnelOptions(rawOptions: options ?? [:])
- let appSelectorResult: Result<RelaySelectorResult?, Error> = Result { try tunnelOptions.getSelectorResult() }
+ var appSelectorResult: RelaySelectorResult?
- switch appSelectorResult {
- case .success(.some(let selectorResult)):
- providerLogger.debug("Start the tunnel via app, connect to \(selectorResult.tunnelConnectionInfo.hostname)")
+ // Parse relay selector from tunnel options.
+ do {
+ appSelectorResult = try tunnelOptions.getSelectorResult()
- case .success(nil):
- if tunnelOptions.isOnDemand() {
- providerLogger.debug("Start the tunnel via on-demand rule")
- } else {
- providerLogger.debug("Start the tunnel via system")
- }
+ switch appSelectorResult {
+ case .some(let selectorResult):
+ providerLogger.debug("Start the tunnel via app, connect to \(selectorResult.tunnelConnectionInfo.hostname).")
- case .failure(let error):
- providerLogger.debug("Start the tunnel via app")
+ case .none:
+ if tunnelOptions.isOnDemand() {
+ providerLogger.debug("Start the tunnel via on-demand rule.")
+ } else {
+ providerLogger.debug("Start the tunnel via system.")
+ }
+ }
+ } catch {
+ providerLogger.debug("Start the tunnel via app.")
providerLogger.error(
chainedError: AnyChainedError(error),
message: "Failed to decode relay selector result passed from the app. Will continue by picking new relay."
)
}
- makeConfiguration(appSelectorResult.flattenValue)
- .asPromise()
- .receive(on: dispatchQueue)
- .mapThen { tunnelConfiguration in
- let tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo
- self.tunnelConnectionInfo = tunnelConnectionInfo
+ // Read tunnel configuration.
+ let tunnelConfiguration: PacketTunnelConfiguration
+ switch makeConfiguration(appSelectorResult) {
+ case .success(let configuration):
+ tunnelConfiguration = configuration
+
+ case .failure(let error):
+ providerLogger.error(chainedError: error, message: "Failed to start the tunnel.")
+ completionHandler(error)
+ return
+ }
+
+ // Set tunnel connection info.
+ dispatchQueue.async {
+ self.tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo
+ }
+
+ // Start tunnel.
+ adapter.start(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in
+ self.dispatchQueue.async {
+ if let error = error {
+ let tunnelProviderError = PacketTunnelProviderError.startWireguardAdapter(error)
+ self.providerLogger.error(chainedError: tunnelProviderError, message: "Failed to start the tunnel.")
- return self.adapter.start(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig)
- .mapError { error in
- return PacketTunnelProviderError.startWireguardAdapter(error)
+ completionHandler(tunnelProviderError)
+ } 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?.isConnected = true
+
+ // Call system completion handler.
+ completionHandler(error)
}
- .receive(on: self.dispatchQueue)
- }
- .onSuccess {
- self.providerLogger.debug("Started the tunnel")
- }
- .onFailure { error in
- self.providerLogger.error(chainedError: error, message: "Failed to start the tunnel")
- }
- .observe { completion in
- completionHandler(completion.unwrappedValue?.error)
+
+ // Start tunnel monitor.
+ let gatewayAddress = tunnelConfiguration.selectorResult.endpoint.ipv4Gateway
+ self.tunnelMonitor.start(address: gatewayAddress)
+ }
}
+ }
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
providerLogger.debug("Stop the tunnel: \(reason)")
- adapter.stop()
- .receive(on: self.dispatchQueue)
- .mapError { error in
- return PacketTunnelProviderError.stopWireguardAdapter(error)
- }
- .onFailure { error in
- self.providerLogger.error(chainedError: error, message: "Failed to stop the tunnel gracefully")
- }
- .observe { _ in
- self.providerLogger.debug("Stopped the tunnel")
+ dispatchQueue.async {
+ // Stop tunnel monitor.
+ self.tunnelMonitor.stop()
+
+ // Unset the start tunnel completion handler.
+ self.startTunnelCompletionHandler = nil
+ }
+
+ adapter.stop { error in
+ self.dispatchQueue.async {
+ let tunnelProviderError = error.map { PacketTunnelProviderError.stopWireguardAdapter($0) }
+
+ if let tunnelProviderError = tunnelProviderError {
+ self.providerLogger.error(chainedError: tunnelProviderError, message: "Failed to stop the tunnel gracefully.")
+ }
+
+ self.providerLogger.debug("Stopped the tunnel.")
completionHandler()
}
+ }
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
- Result { try TunnelIPC.Coding.decodeRequest(messageData) }
- .mapError { PacketTunnelProviderError.ipcHandler($0) }
- .asPromise()
- .onFailure { error in
- self.providerLogger.error(chainedError: error, message: "Failed to decode the app message request")
+ dispatchQueue.async {
+ let request: TunnelIPC.Request
+ do {
+ request = try TunnelIPC.Coding.decodeRequest(messageData)
+ } catch {
+ self.providerLogger.error(chainedError: AnyChainedError(error), message: "Failed to decode the app message request.")
+
+ completionHandler?(nil)
+ return
}
- .receive(on: dispatchQueue)
- .mapThen { request -> Result<Data?, PacketTunnelProviderError>.Promise in
- self.providerLogger.debug("handleAppMessage: \(request)")
- switch request {
- case .reloadTunnelSettings:
- self.reloadTunnelSettings().observe { _ in }
- return .success(nil)
+ self.providerLogger.debug("Received app message: \(request)")
- case .tunnelConnectionInfo:
- return Result { try TunnelIPC.Coding.encodeResponse(self.tunnelConnectionInfo) }
- .mapError { PacketTunnelProviderError.ipcHandler($0) }
- .map { data -> Data? in
- return .some(data)
- }
- .flatMapError { error in
- self.providerLogger.error(chainedError: error, message: "Failed to encode the app message response for \(request)")
- return .success(nil)
- }
- .asPromise()
+ switch request {
+ case .reloadTunnelSettings:
+ self.providerLogger.debug("Reloading tunnel settings...")
+
+ self.reloadTunnelSettings { [weak self] error in
+ guard let self = self else { return }
+
+ if let error = error {
+ self.providerLogger.error(chainedError: error, message: "Failed to reload tunnel settings.")
+ } else {
+ self.providerLogger.debug("Reloaded tunnel settings.")
+ }
}
- }.observe { completion in
- completionHandler?(completion.unwrappedValue?.value ?? nil)
+
+ completionHandler?(nil)
+
+ case .tunnelConnectionInfo:
+ var response: Data?
+ do {
+ response = try TunnelIPC.Coding.encodeResponse(self.tunnelConnectionInfo)
+ } catch {
+ self.providerLogger.error(chainedError: AnyChainedError(error), message: "Failed to encode the app message response for \(request)")
+ }
+
+ completionHandler?(response)
}
+ }
}
override func sleep(completionHandler: @escaping () -> Void) {
@@ -163,53 +225,166 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// Add code here to wake up.
}
+ // MARK: - TunnelMonitor
+
+ func tunnelMonitorDidDetermineConnectionEstablished(_ tunnelMonitor: TunnelMonitor) {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+
+ providerLogger.debug("Connection established.")
+
+ startTunnelCompletionHandler?(nil)
+ startTunnelCompletionHandler = nil
+
+ reassertTunnelCompletionHandler?(nil)
+ reassertTunnelCompletionHandler = nil
+ }
+
+ func tunnelMonitorDelegateShouldHandleConnectionRecovery(_ tunnelMonitor: TunnelMonitor) {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+
+ providerLogger.debug("Recover connection. Picking next relay...")
+
+ let handleRecoveryFailure = { (_ error: PacketTunnelProviderError) 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
+ switch makeConfiguration(nil) {
+ case .success(let configuration):
+ tunnelConfiguration = configuration
+
+ case .failure(let error):
+ handleRecoveryFailure(error)
+ return
+ }
+
+ // Set tunnel connection info.
+ self.tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo
+
+ // Update WireGuard configuration.
+ adapter.update(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in
+ self.dispatchQueue.async {
+ if let error = error {
+ handleRecoveryFailure(.updateWireguardConfiguration(error))
+ }
+ }
+ }
+ }
+
// MARK: - Private
private func makeConfiguration(_ appSelectorResult: RelaySelectorResult? = nil) -> Result<PacketTunnelConfiguration, PacketTunnelProviderError> {
- return protocolConfiguration.passwordReference.map { data in
- return Self.readTunnelSettings(keychainReference: data)
- .flatMap { tunnelSettings in
- return (appSelectorResult.map { .success($0) } ?? Self.selectRelayEndpoint(relayConstraints: tunnelSettings.relayConstraints))
- .map { (selectorResult) -> PacketTunnelConfiguration in
- return PacketTunnelConfiguration(
- tunnelSettings: tunnelSettings,
- selectorResult: selectorResult
- )
- }
- }
- } ?? .failure(.missingKeychainConfigurationReference)
+ guard let passwordRef = protocolConfiguration.passwordReference else {
+ return .failure(.missingKeychainConfigurationReference)
+ }
+
+ let keychainEntry: TunnelSettingsManager.KeychainEntry
+ switch TunnelSettingsManager.load(searchTerm: .persistentReference(passwordRef)) {
+ case .success(let entry):
+ keychainEntry = entry
+ case .failure(let error):
+ return .failure(.cannotReadTunnelSettings(error))
+ }
+
+ let selectorResult: RelaySelectorResult
+ if let appSelectorResult = appSelectorResult {
+ selectorResult = appSelectorResult
+ } else {
+ let relayConstraints = keychainEntry.tunnelSettings.relayConstraints
+ switch Self.selectRelayEndpoint(relayConstraints: relayConstraints) {
+ case .success(let value):
+ selectorResult = value
+ case .failure(let error):
+ return .failure(error)
+ }
+ }
+
+ let tunnelConfiguration = PacketTunnelConfiguration(
+ tunnelSettings: keychainEntry.tunnelSettings,
+ selectorResult: selectorResult
+ )
+
+ return .success(tunnelConfiguration)
}
- private func reloadTunnelSettings() -> Result<(), PacketTunnelProviderError>.Promise {
- providerLogger.debug("Reload tunnel settings")
+ private func reloadTunnelSettings(completionHandler: @escaping (PacketTunnelProviderError?) -> Void) {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
- return makeConfiguration()
- .asPromise()
- .mapThen { packetTunnelConfig in
- let tunnelConnectionInfo = packetTunnelConfig.selectorResult.tunnelConnectionInfo
- let oldTunnelConnectionInfo = self.tunnelConnectionInfo
- self.tunnelConnectionInfo = tunnelConnectionInfo
+ // Read tunnel configuration.
+ let tunnelConfiguration: PacketTunnelConfiguration
+ switch makeConfiguration(nil) {
+ case .success(let configuration):
+ tunnelConfiguration = configuration
- return self.adapter.update(tunnelConfiguration: packetTunnelConfig.wgTunnelConfig)
- .receive(on: self.dispatchQueue)
- .mapError { error in
- return PacketTunnelProviderError.updateWireguardConfiguration(error)
- }
- .onSuccess { _ in
- self.providerLogger.debug("Updated WireGuard configuration")
+ case .failure(let error):
+ completionHandler(error)
+ return
+ }
+
+ // Set tunnel connection info.
+ let tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo
+ let oldTunnelConnectionInfo = self.tunnelConnectionInfo
+ self.tunnelConnectionInfo = tunnelConnectionInfo
+
+ // 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
+
+ // Call completion handler immediately on error to update adapter configuration.
+ if let error = error {
+ // Revert to previously used tunnel connection info.
+ self.tunnelConnectionInfo = oldTunnelConnectionInfo
+
+ // Lower the reasserting flag.
+ if self.isConnected {
+ self.reasserting = false
}
- .onFailure { error in
- self.tunnelConnectionInfo = oldTunnelConnectionInfo
- self.providerLogger.error(chainedError: error)
+
+ // Call completion handler immediately.
+ completionHandler(.updateWireguardConfiguration(error))
+ } 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)
}
- }
- }
- /// Read tunnel settings from Keychain
- private class func readTunnelSettings(keychainReference: Data) -> Result<TunnelSettings, PacketTunnelProviderError> {
- return TunnelSettingsManager.load(searchTerm: .persistentReference(keychainReference))
- .mapError { PacketTunnelProviderError.cannotReadTunnelSettings($0) }
- .map { $0.tunnelSettings }
+ // Restart tunnel monitor.
+ let gatewayAddress = tunnelConfiguration.selectorResult.endpoint.ipv4Gateway
+ self.tunnelMonitor.start(address: gatewayAddress)
+ }
+ }
+ }
}
/// Load relay cache with potential networking to refresh the cache and pick the relay for the
@@ -219,10 +394,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
let prebundledRelaysURL = RelayCache.IO.preBundledRelaysFileURL!
return RelayCache.IO.readWithFallback(cacheFileURL: cacheFileURL, preBundledRelaysFileURL: prebundledRelaysURL)
- .mapError { relayCacheError -> PacketTunnelProviderError in
- return .readRelayCache(relayCacheError)
+ .mapError { error in
+ return PacketTunnelProviderError.readRelayCache(error)
}
- .flatMap { cachedRelayList -> Result<RelaySelectorResult, PacketTunnelProviderError> in
+ .flatMap { cachedRelayList in
if let selectorResult = RelaySelector.evaluate(relays: cachedRelayList.relays, constraints: relayConstraints) {
return .success(selectorResult)
} else {
@@ -254,34 +429,28 @@ enum PacketTunnelProviderError: ChainedError {
/// Failure to update the Wireguard configuration
case updateWireguardConfiguration(WireGuardAdapterError)
- /// IPC handler failure
- case ipcHandler(Error)
-
var errorDescription: String? {
switch self {
case .readRelayCache:
- return "Failure to read the relay cache"
+ return "Failure to read the relay cache."
case .noRelaySatisfyingConstraint:
- return "No relay satisfying the given constraint"
+ return "No relay satisfying the given constraint."
case .missingKeychainConfigurationReference:
- return "Keychain configuration reference is not set on protocol configuration"
+ return "Keychain configuration reference is not set on protocol configuration."
case .cannotReadTunnelSettings:
- return "Failure to read tunnel settings"
+ return "Failure to read tunnel settings."
case .startWireguardAdapter:
- return "Failure to start the WireGuard adapter"
+ return "Failure to start the WireGuard adapter."
case .stopWireguardAdapter:
- return "Failure to stop the WireGuard adapter"
+ return "Failure to stop the WireGuard adapter."
case .updateWireguardConfiguration:
- return "Failure to update the Wireguard configuration"
-
- case .ipcHandler:
- return "Failure to handle the IPC request"
+ return "Failure to update the Wireguard configuration."
}
}
}
@@ -347,32 +516,6 @@ extension WireGuardLogLevel {
}
}
-extension WireGuardAdapter {
- func start(tunnelConfiguration: TunnelConfiguration) -> Result<(), WireGuardAdapterError>.Promise {
- return Result<(), WireGuardAdapterError>.Promise { resolver in
- self.start(tunnelConfiguration: tunnelConfiguration) { error in
- resolver.resolve(value: error.map { .failure($0) } ?? .success(()))
- }
- }
- }
-
- func stop() -> Result<(), WireGuardAdapterError>.Promise {
- return Result<(), WireGuardAdapterError>.Promise { resolver in
- self.stop { error in
- resolver.resolve(value: error.map { .failure($0) } ?? .success(()))
- }
- }
- }
-
- func update(tunnelConfiguration: TunnelConfiguration) -> Result<(), WireGuardAdapterError>.Promise {
- return Result<(), WireGuardAdapterError>.Promise { resolver in
- self.update(tunnelConfiguration: tunnelConfiguration) { error in
- resolver.resolve(value: error.map { .failure($0) } ?? .success(()))
- }
- }
- }
-}
-
extension WireGuardAdapterError: LocalizedError {
public var errorDescription: String? {
switch self {
@@ -393,10 +536,10 @@ extension WireGuardAdapterError: LocalizedError {
return "Failure to resolve endpoints:\n\(detailedErrorDescription)"
case .setNetworkSettings:
- return "Failure to set network settings"
+ return "Failure to set network settings."
case .startWireGuardBackend(let code):
- return "Failure to start WireGuard backend (error code: \(code))"
+ return "Failure to start WireGuard backend (error code: \(code))."
}
}
}
@@ -412,4 +555,3 @@ extension MullvadEndpoint {
return Endpoint(host: .ipv6(ipv6Relay.ip), port: .init(integerLiteral: ipv6Relay.port))
}
}
-
diff --git a/ios/PacketTunnel/Pinger.swift b/ios/PacketTunnel/Pinger.swift
new file mode 100644
index 0000000000..ea143d0ca6
--- /dev/null
+++ b/ios/PacketTunnel/Pinger.swift
@@ -0,0 +1,257 @@
+//
+// Pinger.swift
+// PacketTunnel
+//
+// Created by pronebird on 21/02/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import struct Network.IPv4Address
+import Logging
+
+final class Pinger {
+ // Sender identifier passed along with ICMP packet.
+ private let identifier: UInt16 = 757
+
+ private var sequenceNumber: UInt16 = 0
+ private var socket: CFSocket?
+
+ private let address: IPv4Address
+ private let interfaceName: String?
+
+ 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
+ }
+
+ deinit {
+ stop()
+ }
+
+ func start(delay: DispatchTimeInterval, repeating repeatInterval: DispatchTimeInterval) -> Result<(), Pinger.Error> {
+ stateLock.lock()
+ defer { stateLock.unlock() }
+
+ stop()
+
+ guard let newSocket = CFSocketCreate(kCFAllocatorDefault, AF_INET, SOCK_DGRAM, IPPROTO_ICMP, 0, nil, nil) else {
+ return .failure(.createSocket)
+ }
+
+ let flags = CFSocketGetSocketFlags(newSocket)
+ if (flags & kCFSocketCloseOnInvalidate) == 0 {
+ CFSocketSetSocketFlags(newSocket, flags | kCFSocketCloseOnInvalidate)
+ }
+
+ if case .failure(let error) = bindSocket(newSocket) {
+ return .failure(error)
+ }
+
+ guard let runLoop = CFSocketCreateRunLoopSource(kCFAllocatorDefault, newSocket, 0) else {
+ return .failure(.createRunLoop)
+ }
+
+ CFRunLoopAddSource(CFRunLoopGetMain(), runLoop, .defaultMode)
+
+ let newTimer = DispatchSource.makeTimerSource()
+ newTimer.setEventHandler { [weak self] in
+ self?.send()
+ }
+
+ socket = newSocket
+ timer = newTimer
+
+ newTimer.schedule(wallDeadline: .now() + delay, repeating: repeatInterval)
+ newTimer.resume()
+
+ return .success(())
+ }
+
+ func stop() {
+ stateLock.lock()
+ defer { stateLock.unlock() }
+
+ if let socket = socket {
+ CFSocketInvalidate(socket)
+ }
+
+ socket = nil
+
+ timer?.cancel()
+ timer = nil
+ }
+
+ private func send() {
+ stateLock.lock()
+ guard let socket = socket else {
+ stateLock.unlock()
+ return
+ }
+ stateLock.unlock()
+
+ var sa = sockaddr_in()
+ sa.sin_len = UInt8(MemoryLayout.size(ofValue: sa))
+ sa.sin_family = sa_family_t(AF_INET)
+ sa.sin_addr = address.rawValue.withUnsafeBytes { buffer in
+ return buffer.bindMemory(to: in_addr.self).baseAddress!.pointee
+ }
+
+ let sequenceNumber = nextSequenceNumber()
+ let packetData = Self.createICMPPacket(identifier: identifier, sequenceNumber: sequenceNumber, payload: nil)
+
+ let bytesSent = packetData.withUnsafeBytes { dataBuffer -> Int in
+ return withUnsafeBytes(of: &sa) { bufferPointer in
+ let sockaddrPointer = bufferPointer.bindMemory(to: sockaddr.self).baseAddress!
+
+ return sendto(
+ CFSocketGetNative(socket),
+ dataBuffer.baseAddress!,
+ dataBuffer.count,
+ 0,
+ sockaddrPointer,
+ socklen_t(MemoryLayout<sockaddr_in>.size)
+ )
+ }
+ }
+
+ if bytesSent == -1 {
+ logger.debug("Failed to send echo (errno: \(errno)).")
+ }
+ }
+
+ private func nextSequenceNumber() -> UInt16 {
+ stateLock.lock()
+ let (partialValue, isOverflow) = sequenceNumber.addingReportingOverflow(1)
+ let nextSequenceNumber = isOverflow ? 0 : partialValue
+
+ sequenceNumber = nextSequenceNumber
+ stateLock.unlock()
+
+ return nextSequenceNumber
+ }
+
+ private func bindSocket(_ socket: CFSocket) -> Result<(), Pinger.Error> {
+ guard let interfaceName = interfaceName else {
+ logger.debug("Interface is not specified.")
+ return .success(())
+ }
+
+ var index = if_nametoindex(interfaceName)
+ guard index > 0 else {
+ return .failure(.mapInterfaceNameToIndex(errno))
+ }
+
+ logger.debug("Bind socket to \"\(interfaceName)\" (index: \(index))...")
+
+ let result = setsockopt(
+ CFSocketGetNative(socket),
+ IPPROTO_IP,
+ IP_BOUND_IF,
+ &index,
+ socklen_t(MemoryLayout.size(ofValue: index))
+ )
+
+ if result == -1 {
+ logger.error("Failed to bind socket to \"\(interfaceName)\" (index: \(index), errno: \(errno)).")
+
+ return .failure(.bindSocket(errno))
+ } else {
+ return .success(())
+ }
+ }
+
+ 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)) }
+
+ // Identifier.
+ withUnsafeBytes(of: identifier.bigEndian) { data.append(Data($0)) }
+
+ // Sequence number.
+ withUnsafeBytes(of: sequenceNumber.bigEndian) { data.append(Data($0)) }
+
+ // Append payload.
+ if let payload = payload {
+ data.append(contentsOf: payload)
+ }
+
+ // Calculate checksum.
+ let checksum = in_chksum(data)
+
+ // Inject computed checksum into the packet.
+ data.withUnsafeMutableBytes { buffer in
+ buffer.storeBytes(of: checksum, toByteOffset: 2, as: UInt16.self)
+ }
+
+ return data
+ }
+}
+
+extension Pinger {
+ enum Error: LocalizedError, Equatable {
+ /// Failure to create a socket.
+ case createSocket
+
+ /// Failure to map interface name to index.
+ case mapInterfaceNameToIndex(Int32)
+
+ /// Failure to bind socket to interface.
+ case bindSocket(Int32)
+
+ /// Failure to create a runloop for socket.
+ case createRunLoop
+
+ var errorDescription: String? {
+ switch self {
+ case .createSocket:
+ return "Failure to create socket."
+ case .mapInterfaceNameToIndex:
+ return "Failure to map interface name to index."
+ case .bindSocket:
+ return "Failure to bind socket to interface."
+ case .createRunLoop:
+ return "Failure to create run loop for socket."
+ }
+ }
+ }
+}
+
+private func in_chksum(_ data: Data) -> UInt16 {
+ return data.withUnsafeBytes { buffer in
+ let length = buffer.count
+
+ var sum: Int32 = 0
+
+ let isOdd = length % 2 != 0
+ let strideTo = isOdd ? length - 1 : length
+
+ for offset in stride(from: 0, to: strideTo, by: 2) {
+ let word = buffer.load(fromByteOffset: offset, as: UInt16.self)
+ sum += Int32(word)
+ }
+
+ if isOdd {
+ let byte = buffer.load(fromByteOffset: length - 1, as: UInt8.self)
+ sum += Int32(byte)
+ }
+
+ sum = (sum >> 16) + (sum & 0xffff)
+ sum += (sum >> 16)
+
+ return UInt16(truncatingIfNeeded: ~sum)
+ }
+}
diff --git a/ios/PacketTunnel/TunnelMonitor.swift b/ios/PacketTunnel/TunnelMonitor.swift
new file mode 100644
index 0000000000..2c5049f919
--- /dev/null
+++ b/ios/PacketTunnel/TunnelMonitor.swift
@@ -0,0 +1,312 @@
+//
+// TunnelMonitor.swift
+// PacketTunnel
+//
+// Created by pronebird on 09/02/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import NetworkExtension
+import WireGuardKit
+import Logging
+
+protocol TunnelMonitorDelegate: AnyObject {
+ /// Invoked when tunnel monitor determined that connection is established.
+ func tunnelMonitorDidDetermineConnectionEstablished(_ tunnelMonitor: TunnelMonitor)
+
+ /// Invoked when tunnel monitor determined that connection attempt has failed.
+ func tunnelMonitorDelegateShouldHandleConnectionRecovery(_ tunnelMonitor: TunnelMonitor)
+
+ /// Invoked when network reachability status changes.
+ func tunnelMonitor(_ tunnelMonitor: TunnelMonitor, networkReachabilityStatusDidChange isNetworkReachable: Bool)
+}
+
+final class TunnelMonitor {
+ private let adapter: WireGuardAdapter
+ private let internalQueue = DispatchQueue(label: "TunnelMonitor")
+ private let delegateQueue: DispatchQueue
+
+ private var address: IPv4Address?
+ private var 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 isPinging = false
+
+ private var logger = Logger(label: "TunnelMonitor")
+ private var timer: DispatchSourceTimer?
+
+ private weak var _delegate: TunnelMonitorDelegate?
+ weak var delegate: TunnelMonitorDelegate? {
+ set {
+ internalQueue.sync {
+ _delegate = newValue
+ }
+ }
+ get {
+ return internalQueue.sync {
+ return _delegate
+ }
+ }
+ }
+
+ var startDate: Date? {
+ return internalQueue.sync {
+ return firstAttemptDate
+ }
+ }
+
+ init(queue: DispatchQueue, adapter: WireGuardAdapter) {
+ delegateQueue = queue
+ self.adapter = adapter
+ }
+
+ deinit {
+ stopNoQueue(forRestart: false)
+ }
+
+ func start(address: IPv4Address) {
+ internalQueue.async {
+ self.startNoQueue(address: address)
+ }
+ }
+
+ func stop() {
+ internalQueue.async {
+ self.stopNoQueue(forRestart: false)
+ }
+ }
+
+ private func startNoQueue(address pingAddress: IPv4Address) {
+ if address == nil {
+ logger.debug("Start tunnel monitor with address: \(pingAddress).")
+ } else {
+ logger.debug("Restart tunnel monitor with address: \(pingAddress).")
+ }
+
+ stopNoQueue(forRestart: true)
+
+ address = pingAddress
+ networkBytesReceived = 0
+ firstAttemptDate = Date()
+ lastAttemptDate = firstAttemptDate
+ lastError = nil
+
+ let newPathMonitor = NWPathMonitor()
+ newPathMonitor.pathUpdateHandler = { [weak self] path in
+ self?.handleNetworkPathUpdate(path)
+ }
+ newPathMonitor.start(queue: internalQueue)
+ pathMonitor = newPathMonitor
+
+ handleNetworkPathUpdate(newPathMonitor.currentPath)
+ }
+
+ private func stopNoQueue(forRestart: Bool) {
+ if !forRestart {
+ logger.debug("Stop tunnel monitor.")
+ }
+
+ address = nil
+ firstAttemptDate = nil
+ lastAttemptDate = nil
+ lastError = nil
+
+ pathMonitor?.cancel()
+ pathMonitor = nil
+
+ cancelWgStatsTimer()
+ stopPinging()
+ }
+
+ private func startPinging(address: IPv4Address) -> Result<(), Pinger.Error> {
+ let newPinger = Pinger(address: address, interfaceName: adapter.interfaceName)
+ let pingerResult = newPinger.start(delay: TunnelMonitorConfiguration.pingStartDelay, repeating: TunnelMonitorConfiguration.pingInterval)
+
+ if case .success = pingerResult {
+ pinger = newPinger
+ isPinging = true
+ }
+
+ return pingerResult
+ }
+
+ private func stopPinging() {
+ pinger?.stop()
+ pinger = nil
+
+ isPinging = false
+ }
+
+ private func setWgStatsTimer() {
+ // Cancel existing timer.
+ cancelWgStatsTimer()
+
+ // Create new timer.
+ timer = DispatchSource.makeTimerSource(queue: internalQueue)
+ timer?.setEventHandler { [weak self] in
+ self?.onWgStatsTimer()
+ }
+ timer?.schedule(wallDeadline: .now(), repeating: TunnelMonitorConfiguration.wgStatsQueryInterval)
+ timer?.resume()
+
+ logger.debug("Set WG stats timer.")
+ }
+
+ private func cancelWgStatsTimer() {
+ timer?.cancel()
+ timer = nil
+ }
+
+ private func onWgStatsTimer() {
+ adapter.getRuntimeConfiguration { [weak self] str in
+ guard let self = self else { return }
+
+ self.internalQueue.async {
+ self.handleWgStatsUpdate(string: str)
+ }
+ }
+ }
+
+ private func handleWgStatsUpdate(string: String?) {
+ guard let string = string else {
+ logger.debug("Received no runtime configuration from WireGuard adapter.")
+ return
+ }
+
+ guard let newNetworkBytesReceived = Self.parseNetworkBytesReceived(from: string) else {
+ logger.debug("Failed to parse rx_bytes from runtime configuration.")
+ return
+ }
+
+ let oldNetworkBytesReceived = self.networkBytesReceived
+ networkBytesReceived = newNetworkBytesReceived
+
+ if newNetworkBytesReceived < oldNetworkBytesReceived {
+ logger.debug("Stats was reset? newNetworkBytesReceived = \(newNetworkBytesReceived), oldNetworkBytesReceived = \(oldNetworkBytesReceived)")
+ return
+ }
+
+ if newNetworkBytesReceived > oldNetworkBytesReceived {
+ // Tell delegate that connection is established.
+ delegateQueue.async {
+ self.delegate?.tunnelMonitorDidDetermineConnectionEstablished(self)
+ }
+
+ // Stop the tunnel monitor.
+ stopNoQueue(forRestart: false)
+
+ return
+ }
+
+ if let nextAttemptDate = lastAttemptDate?.addingTimeInterval(TunnelMonitorConfiguration.connectionTimeout), nextAttemptDate <= Date() {
+ // Reset the last recovery attempt date.
+ lastAttemptDate = nextAttemptDate
+
+ // Reset last error.
+ lastError = nil
+
+ // Tell delegate to attempt the connection recovery.
+ delegateQueue.async {
+ self.delegate?.tunnelMonitorDelegateShouldHandleConnectionRecovery(self)
+ }
+ }
+ }
+
+ private func handleNetworkPathUpdate(_ networkPath: Network.NWPath) {
+ guard let address = address else {
+ return
+ }
+
+ let isNetworkReachable = isNetworkPathReachable(networkPath)
+
+ switch (isNetworkReachable, isPinging) {
+ case (true, false):
+ logger.debug("Network is reachable. Starting to ping.")
+
+ switch startPinging(address: address) {
+ case .success:
+ // Reset the last recovery attempt date.
+ firstAttemptDate = Date()
+ lastAttemptDate = firstAttemptDate
+
+ // Start WG stats timer.
+ setWgStatsTimer()
+
+ delegateQueue.async {
+ self.delegate?.tunnelMonitor(self, networkReachabilityStatusDidChange: isNetworkReachable)
+ }
+
+ case .failure(let error):
+ if error != lastError {
+ logger.error(chainedError: AnyChainedError(error), message: "Failed to start pinging.")
+ lastError = error
+ }
+ }
+
+ case (false, true):
+ logger.debug("Network is unreachable. Stop pinging and wait...")
+
+ // Cancel timers and ping.
+ cancelWgStatsTimer()
+ stopPinging()
+
+ // Reset the last recovery attempt date.
+ lastAttemptDate = nil
+
+ delegateQueue.async {
+ self.delegate?.tunnelMonitor(self, networkReachabilityStatusDidChange: isNetworkReachable)
+ }
+
+ default:
+ break
+ }
+ }
+
+ private func isNetworkPathReachable(_ networkPath: Network.NWPath) -> Bool {
+ // Get utun interface name.
+ guard let tunName = adapter.interfaceName else { return false }
+
+ // Check if utun is up.
+ let utunUp = networkPath.availableInterfaces.contains { interface in
+ return interface.name == tunName
+ }
+
+ // Return false if tunnel is down.
+ guard utunUp else {
+ return false
+ }
+
+ // Return false if utun is the only available interface.
+ if networkPath.availableInterfaces.count == 1 {
+ return false
+ }
+
+ switch networkPath.status {
+ case .requiresConnection, .satisfied:
+ return true
+ case .unsatisfied:
+ return false
+ @unknown default:
+ 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/TunnelMonitorConfiguration.swift b/ios/PacketTunnel/TunnelMonitorConfiguration.swift
new file mode 100644
index 0000000000..13f4c4908d
--- /dev/null
+++ b/ios/PacketTunnel/TunnelMonitorConfiguration.swift
@@ -0,0 +1,23 @@
+//
+// 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
+}