diff options
| -rw-r--r-- | ios/MullvadLogging/LogFileOutputStream.swift | 168 | ||||
| -rw-r--r-- | ios/MullvadLogging/Logging.swift | 115 | ||||
| -rw-r--r-- | ios/MullvadLogging/TextFileOutputStream.swift | 87 | ||||
| -rw-r--r-- | ios/MullvadTypes/KeychainError.swift | 1 | ||||
| -rw-r--r-- | ios/MullvadTypes/PacketTunnelErrorWrapper.swift | 46 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 8 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 21 | ||||
| -rw-r--r-- | ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift | 8 | ||||
| -rw-r--r-- | ios/MullvadVPN/ProblemReportReviewViewController.swift | 34 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelProvider.swift | 197 |
10 files changed, 437 insertions, 248 deletions
diff --git a/ios/MullvadLogging/LogFileOutputStream.swift b/ios/MullvadLogging/LogFileOutputStream.swift new file mode 100644 index 0000000000..0ffd434c65 --- /dev/null +++ b/ios/MullvadLogging/LogFileOutputStream.swift @@ -0,0 +1,168 @@ +// +// LogFileOutputStream.swift +// MullvadVPN +// +// Created by pronebird on 02/08/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Interval used for reopening the log file descriptor in the event of failure to open it in +/// the first place, or when writing to it. +private let reopenFileLogInterval: TimeInterval = 5 + +class LogFileOutputStream: TextOutputStream { + private let queue = DispatchQueue(label: "LogFileOutputStreamQueue", qos: .utility) + + private let fileURL: URL + private let encoding: String.Encoding + private let maxBufferCapacity: Int + + private var state: State = .closed { + didSet { + switch (oldValue, state) { + case (.opened, .waitingToReopen), (.closed, .waitingToReopen): + startTimer() + + case (.waitingToReopen, .opened), (.waitingToReopen, .closed): + stopTimer() + + default: + break + } + } + } + + private var timer: DispatchSourceTimer? + private var buffer = Data() + + private enum State { + case closed + case opened(FileHandle) + case waitingToReopen + } + + init(fileURL: URL, encoding: String.Encoding = .utf8, maxBufferCapacity: Int = 16 * 1024) { + self.fileURL = fileURL + self.encoding = encoding + self.maxBufferCapacity = maxBufferCapacity + } + + deinit { + stopTimer() + } + + func write(_ string: String) { + queue.async { + self.writeNoQueue(string) + } + } + + private func writeNoQueue(_ string: String) { + guard let data = string.data(using: encoding) else { return } + + switch state { + case .closed: + do { + let fileHandle = try openFile() + state = .opened(fileHandle) + try write(fileHandle: fileHandle, data: data) + } catch { + bufferData(data) + state = .waitingToReopen + } + + case let .opened(fileHandle): + do { + try write(fileHandle: fileHandle, data: data) + } catch { + bufferData(data) + state = .waitingToReopen + } + + case .waitingToReopen: + bufferData(data) + } + } + + @discardableResult private func write(fileHandle: FileHandle, data: Data) throws -> Int { + let bytesWritten = data.withUnsafeBytes { buffer -> Int in + guard let ptr = buffer.baseAddress else { return 0 } + + return Darwin.write(fileHandle.fileDescriptor, ptr, buffer.count) + } + + if bytesWritten == -1 { + let code = POSIXErrorCode(rawValue: errno)! + throw POSIXError(code) + } else { + return bytesWritten + } + } + + private func openFile() throws -> FileHandle { + let oflag: Int32 = O_WRONLY | O_CREAT + let mode: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH + + let fd = fileURL.path.withCString { Darwin.open($0, oflag, mode) } + + if fd == -1 { + let code = POSIXErrorCode(rawValue: errno)! + throw POSIXError(code) + } else { + return FileHandle(fileDescriptor: fd, closeOnDealloc: true) + } + } + + private func startTimer() { + timer?.cancel() + + let timer = DispatchSource.makeTimerSource(queue: queue) + timer.setEventHandler { [weak self] in + self?.reopenFile() + } + timer.schedule( + wallDeadline: .now() + reopenFileLogInterval, + repeating: reopenFileLogInterval + ) + timer.activate() + + self.timer = timer + } + + private func stopTimer() { + timer?.cancel() + timer = nil + } + + private func reopenFile() { + do { + let fileHandle = try openFile() + + // Write a message indicating that the file was reopened. + let messageData = + "<Log file re-opened after failure. Buffered \(buffer.count) bytes of messages>\n" + .data(using: encoding, allowLossyConversion: true)! + try write(fileHandle: fileHandle, data: messageData) + + // Write all buffered messages. + if !buffer.isEmpty { + try write(fileHandle: fileHandle, data: buffer) + buffer.removeAll() + } + + state = .opened(fileHandle) + } catch { + state = .waitingToReopen + } + } + + private func bufferData(_ data: Data) { + buffer.append(data) + + if buffer.count > maxBufferCapacity { + buffer.removeFirst(buffer.count - maxBufferCapacity) + } + } +} diff --git a/ios/MullvadLogging/Logging.swift b/ios/MullvadLogging/Logging.swift index 9120dcb886..be5c46b08b 100644 --- a/ios/MullvadLogging/Logging.swift +++ b/ios/MullvadLogging/Logging.swift @@ -9,71 +9,82 @@ import Foundation @_exported import Logging -public func initLoggingSystem( - bundleIdentifier: String, - applicationGroupIdentifier: String, - metadata: Logger.Metadata? = nil -) { - let containerURL = FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: applicationGroupIdentifier - )! - let logsDirectoryURL = containerURL.appendingPathComponent("Logs", isDirectory: true) - let logFileName = "\(bundleIdentifier).log" - let logFileURL = logsDirectoryURL.appendingPathComponent(logFileName) +private enum LoggerOutput { + case fileOutput(_ fileOutput: LogFileOutputStream) + case osLogOutput(_ subsystem: String) +} + +public struct MissingSharedContainerError: LocalizedError { + public var errorDescription: String? { + return "Cannot obtain shared container URL." + } +} + +public struct LoggerBuilder { + private(set) var logRotationErrors: [Error] = [] + private var outputs: [LoggerOutput] = [] - // Create Logs folder within container if it doesn't exist - try? FileManager.default.createDirectory( - at: logsDirectoryURL, - withIntermediateDirectories: false, - attributes: nil - ) + public var metadata: Logger.Metadata = [:] - // Rotate log - var logRotationError: Error? - do { - try LogRotation.rotateLog( - logsDirectory: logsDirectoryURL, - logFileName: logFileName + public init() {} + + public mutating func addFileOutput(securityGroupIdentifier: String, basename: String) throws { + guard let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: securityGroupIdentifier + ) else { + throw MissingSharedContainerError() + } + + let logsDirectoryURL = containerURL.appendingPathComponent("Logs", isDirectory: true) + let logFileName = "\(basename).log" + let logFileURL = logsDirectoryURL.appendingPathComponent(logFileName, isDirectory: false) + + try? FileManager.default.createDirectory( + at: logsDirectoryURL, + withIntermediateDirectories: false, + attributes: nil ) - } catch { - logRotationError = error - } - // Create an array of log output streams - var streams: [TextOutputStream] = [] + do { + try LogRotation.rotateLog(logsDirectory: logsDirectoryURL, logFileName: logFileName) + } catch { + logRotationErrors.append(error) + } - // Create output stream to file - if let fileLogStream = TextFileOutputStream(fileURL: logFileURL, createFile: true) { - streams.append(fileLogStream) + outputs.append(.fileOutput(LogFileOutputStream(fileURL: logFileURL))) } - // Configure Logging system - LoggingSystem.bootstrap { label -> LogHandler in - var logHandlers: [LogHandler] = [] + public mutating func addOSLogOutput(subsystem: String) { + outputs.append(.osLogOutput(subsystem)) + } - #if DEBUG - logHandlers.append(OSLogHandler(subsystem: bundleIdentifier, category: label)) - #endif + public func install() { + LoggingSystem.bootstrap { label -> LogHandler in + let logHandlers: [LogHandler] = outputs.map { output in + switch output { + case let .fileOutput(stream): + return CustomFormatLogHandler(label: label, streams: [stream]) - if !streams.isEmpty { - logHandlers.append(CustomFormatLogHandler(label: label, streams: streams)) - } + case let .osLogOutput(subsystem): + return OSLogHandler(subsystem: subsystem, category: label) + } + } - if logHandlers.isEmpty { - return SwiftLogNoOpLogHandler() - } else { - var multiplex = MultiplexLogHandler(logHandlers) - if let metadata = metadata { + if logHandlers.isEmpty { + return SwiftLogNoOpLogHandler() + } else { + var multiplex = MultiplexLogHandler(logHandlers) multiplex.metadata = metadata + return multiplex } - return multiplex } - } - if let logRotationError = logRotationError { - Logger(label: "LogRotation").error( - error: logRotationError, - message: "Failed to rotate log" - ) + if !logRotationErrors.isEmpty { + let rotationLogger = Logger(label: "LogRotation") + + for error in logRotationErrors { + rotationLogger.error(error: error, message: "Failed to rotate log") + } + } } } diff --git a/ios/MullvadLogging/TextFileOutputStream.swift b/ios/MullvadLogging/TextFileOutputStream.swift deleted file mode 100644 index 95387b53ec..0000000000 --- a/ios/MullvadLogging/TextFileOutputStream.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// TextFileOutputStream.swift -// MullvadVPN -// -// Created by pronebird on 02/08/2020. -// Copyright © 2020 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -class TextFileOutputStream: TextOutputStream { - private let writer: DispatchIO - private let encoding: String.Encoding - private let queue = DispatchQueue.global(qos: .utility) - - class func standardOutputStream(encoding: String.Encoding = .utf8) -> TextFileOutputStream { - return TextFileOutputStream( - fileDescriptor: FileHandle.standardOutput.fileDescriptor, - encoding: encoding - ) - } - - init(fileDescriptor: Int32, encoding: String.Encoding = .utf8) { - self.encoding = encoding - writer = DispatchIO(type: .stream, fileDescriptor: fileDescriptor, queue: queue) { errno in - if errno != 0 { - print("TextFileOutputStream: closed channel with error: \(errno)") - } - } - } - - init?( - fileURL: URL, - createFile: Bool, - filePermissions: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, - encoding: String.Encoding = .utf8 - ) { - var oflag: Int32 = O_WRONLY - var mode: mode_t = .zero - if createFile { - oflag |= O_CREAT - mode = filePermissions - } - - let queue = queue - let writer = fileURL.path.withCString { filePathPointer -> DispatchIO? in - return DispatchIO( - type: .stream, - path: filePathPointer, - oflag: oflag, - mode: mode, - queue: queue, - cleanupHandler: { errno in - if errno != 0 { - print("TextFileOutputStream: closed channel with error: \(errno)") - } - } - ) - } - - if let writer = writer { - self.writer = writer - self.encoding = encoding - } else { - return nil - } - } - - deinit { - writer.close() - } - - func write(_ string: String) { - string.data(using: encoding)?.withUnsafeBytes { bytes in - writer - .write( - offset: .zero, - data: DispatchData(bytes: bytes), - queue: queue - ) { done, data, errno in - if errno != 0 { - print("TextFileOutputStream: write error: \(errno)") - } - } - } - } -} diff --git a/ios/MullvadTypes/KeychainError.swift b/ios/MullvadTypes/KeychainError.swift index 5668a31564..d0766bdc9c 100644 --- a/ios/MullvadTypes/KeychainError.swift +++ b/ios/MullvadTypes/KeychainError.swift @@ -21,6 +21,7 @@ public struct KeychainError: LocalizedError, Equatable { public static let duplicateItem = KeychainError(code: errSecDuplicateItem) public static let itemNotFound = KeychainError(code: errSecItemNotFound) + public static let interactionNotAllowed = KeychainError(code: errSecInteractionNotAllowed) public static func == (lhs: KeychainError, rhs: KeychainError) -> Bool { return lhs.code == rhs.code diff --git a/ios/MullvadTypes/PacketTunnelErrorWrapper.swift b/ios/MullvadTypes/PacketTunnelErrorWrapper.swift index e70bdb0c9f..381b64898a 100644 --- a/ios/MullvadTypes/PacketTunnelErrorWrapper.swift +++ b/ios/MullvadTypes/PacketTunnelErrorWrapper.swift @@ -9,31 +9,41 @@ import Foundation public enum PacketTunnelErrorWrapper: Codable, Equatable, LocalizedError { - /// Failure that indicates wire guard errors. - case wireguard(error: String) + public enum ConfigurationFailureCause: Codable, Equatable { + /// Device is locked. + case deviceLocked + + /// Settings schema is outdated. + case outdatedSchema + + /// No relay satisfying constraints. + case noRelaysSatisfyingConstraints + + /// Read error. + case readFailure + } + + /// Failure that indicates WireGuard errors. + case wireguard(String) /// Failure to read stored settings. - case readConfiguration + case configuration(ConfigurationFailureCause) public var errorDescription: String? { switch self { case let .wireguard(error): return error - case .readConfiguration: - return "Failure to read settings." - } - } - - public static func == (lhs: PacketTunnelErrorWrapper, rhs: PacketTunnelErrorWrapper) -> Bool { - switch (lhs, rhs) { - case (.readConfiguration, .readConfiguration): - return true - - case let (.wireguard(error: lhsError), .wireguard(error: rhsError)): - return lhsError == rhsError - - default: - return false + case let .configuration(cause): + switch cause { + case .deviceLocked: + return "Device is locked." + case .outdatedSchema: + return "Settings schema is outdated." + case .readFailure: + return "Failure to read VPN configuration." + case .noRelaysSatisfyingConstraints: + return "No relays satisfying constraints." + } } } } diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index b4d42f55c9..44ab78144f 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -274,7 +274,7 @@ 58D223F9294C8FF00029F5F8 /* MullvadLogging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223F3294C8FF00029F5F8 /* MullvadLogging.framework */; }; 58D223FA294C8FF10029F5F8 /* MullvadLogging.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223F3294C8FF00029F5F8 /* MullvadLogging.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 58D223FE294C90050029F5F8 /* Error+LogFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581943E128F8010300B0CB5E /* Error+LogFormat.swift */; }; - 58D223FF294C90050029F5F8 /* TextFileOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581943DE28F8010300B0CB5E /* TextFileOutputStream.swift */; }; + 58D223FF294C90050029F5F8 /* LogFileOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581943DE28F8010300B0CB5E /* LogFileOutputStream.swift */; }; 58D22400294C90050029F5F8 /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581943E428F8010400B0CB5E /* OSLogHandler.swift */; }; 58D22401294C90050029F5F8 /* CustomFormatLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581943E328F8010400B0CB5E /* CustomFormatLogHandler.swift */; }; 58D22402294C90050029F5F8 /* Logger+Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581943E028F8010300B0CB5E /* Logger+Errors.swift */; }; @@ -659,7 +659,7 @@ 581813A428E09DE2002817DE /* BlockCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockCondition.swift; sourceTree = "<group>"; }; 581813A628E09DF2002817DE /* MutuallyExclusive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutuallyExclusive.swift; sourceTree = "<group>"; }; 581943DD28F8010300B0CB5E /* LogRotation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogRotation.swift; sourceTree = "<group>"; }; - 581943DE28F8010300B0CB5E /* TextFileOutputStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFileOutputStream.swift; sourceTree = "<group>"; }; + 581943DE28F8010300B0CB5E /* LogFileOutputStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogFileOutputStream.swift; sourceTree = "<group>"; }; 581943DF28F8010300B0CB5E /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; }; 581943E028F8010300B0CB5E /* Logger+Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Logger+Errors.swift"; sourceTree = "<group>"; }; 581943E128F8010300B0CB5E /* Error+LogFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Error+LogFormat.swift"; sourceTree = "<group>"; }; @@ -1561,7 +1561,7 @@ 581943DF28F8010300B0CB5E /* Logging.swift */, 581943DD28F8010300B0CB5E /* LogRotation.swift */, 581943E428F8010400B0CB5E /* OSLogHandler.swift */, - 581943DE28F8010300B0CB5E /* TextFileOutputStream.swift */, + 581943DE28F8010300B0CB5E /* LogFileOutputStream.swift */, ); path = MullvadLogging; sourceTree = "<group>"; @@ -2545,7 +2545,7 @@ 58D22404294C90050029F5F8 /* Date+LogFormat.swift in Sources */, 58D22403294C90050029F5F8 /* Logging.swift in Sources */, 58D223FE294C90050029F5F8 /* Error+LogFormat.swift in Sources */, - 58D223FF294C90050029F5F8 /* TextFileOutputStream.swift in Sources */, + 58D223FF294C90050029F5F8 /* LogFileOutputStream.swift in Sources */, 58D22400294C90050029F5F8 /* OSLogHandler.swift in Sources */, 58D22405294C90050029F5F8 /* LogRotation.swift in Sources */, 58D22401294C90050029F5F8 /* CustomFormatLogHandler.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 945d45d3eb..89b9b505f2 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -47,10 +47,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - initLoggingSystem( - bundleIdentifier: Bundle.main.bundleIdentifier!, - applicationGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier - ) + configureLogging() logger = Logger(label: "AppDelegate") @@ -302,6 +299,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Private + private func configureLogging() { + var loggerBuilder = LoggerBuilder() + let bundleIdentifier = Bundle.main.bundleIdentifier! + + try? loggerBuilder.addFileOutput( + securityGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier, + basename: bundleIdentifier + ) + + #if DEBUG + loggerBuilder.addOSLogOutput(subsystem: bundleIdentifier) + #endif + + loggerBuilder.install() + } + private func addApplicationNotifications(application: UIApplication) { let notificationCenter = NotificationCenter.default diff --git a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift index 33642621d3..52f3b4d75c 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift @@ -53,13 +53,7 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific private func handleTunnelStatus(_ tunnelStatus: TunnelStatus) { let invalidateForTunnelError = updateLastTunnelError( - tunnelStatus.packetTunnelStatus.lastErrors.first(where: { - if case .wireguard = $0 { - return true - } - - return false - })?.localizedDescription + tunnelStatus.packetTunnelStatus.lastErrors.first?.localizedDescription ) let invalidateForManagerError = updateTunnelManagerError(tunnelStatus.state) let invalidateForConnectivity = updateConnectivity(tunnelStatus.state) diff --git a/ios/MullvadVPN/ProblemReportReviewViewController.swift b/ios/MullvadVPN/ProblemReportReviewViewController.swift index 9a4d69b5d8..4effda0bc5 100644 --- a/ios/MullvadVPN/ProblemReportReviewViewController.swift +++ b/ios/MullvadVPN/ProblemReportReviewViewController.swift @@ -12,14 +12,6 @@ class ProblemReportReviewViewController: UIViewController { private var textView = UITextView() private let reportString: String - private var dismissButtonItem: UIBarButtonItem { - return UIBarButtonItem( - barButtonSystemItem: .done, - target: self, - action: #selector(handleDismissButton(_:)) - ) - } - init(reportString: String) { self.reportString = reportString super.init(nibName: nil, bundle: nil) @@ -38,7 +30,20 @@ class ProblemReportReviewViewController: UIViewController { value: "App logs", comment: "" ) - navigationItem.rightBarButtonItem = dismissButtonItem + + navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(handleDismissButton(_:)) + ) + + #if DEBUG + navigationItem.leftBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .action, + target: self, + action: #selector(share(_:)) + ) + #endif textView.translatesAutoresizingMaskIntoConstraints = false textView.text = reportString @@ -71,4 +76,15 @@ class ProblemReportReviewViewController: UIViewController { @objc func handleDismissButton(_ sender: Any) { dismiss(animated: true) } + + #if DEBUG + @objc func share(_ sender: Any) { + let activityController = UIActivityViewController( + activityItems: [reportString], + applicationActivities: nil + ) + + present(activityController, animated: true) + } + #endif } diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 13f7646b9f..b8382e6f3f 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -18,6 +18,9 @@ import RelaySelector import TunnelProviderMessaging import WireGuardKit +/// Restart interval (in seconds) for the tunnel that failed to start early on. +private let tunnelStartupFailureRestartInterval: TimeInterval = 2 + class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { /// Tunnel provider logger. private let providerLogger: Logger @@ -45,10 +48,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { private var numberOfFailedAttempts: UInt = 0 /// Last wireguard error. - private var wgError: PacketTunnelErrorWrapper? + private var wgError: WireGuardAdapterError? + + /// Last configuration read error. + private var configurationError: Error? - /// Last tunnel provider error. - private var tunnelProviderError: PacketTunnelErrorWrapper? + /// Repeating timer used for restarting the tunnel if it had failed during the startup sequence. + private var tunnelStartupFailureRecoveryTimer: DispatchSourceTimer? /// Relay cache. private let relayCache = RelayCache( @@ -68,10 +74,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { /// connection is established. private var startTunnelCompletionHandler: (() -> Void)? - /// A completion handler passed during reassertion and saved for later use once the connection - /// is reestablished. - private var reassertTunnelCompletionHandler: (() -> Void)? - /// Tunnel monitor. private var tunnelMonitor: TunnelMonitor! @@ -89,8 +91,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { /// Returns `PacketTunnelStatus` used for sharing with main bundle process. private var packetTunnelStatus: PacketTunnelStatus { + let errors: [PacketTunnelErrorWrapper?] = [ + wgError.flatMap { PacketTunnelErrorWrapper(error: $0) }, + configurationError.flatMap { PacketTunnelErrorWrapper(error: $0) }, + ] + return PacketTunnelStatus( - lastErrors: [wgError, tunnelProviderError].compactMap { $0 }, + lastErrors: errors.compactMap { $0 }, isNetworkReachable: isNetworkReachable, deviceCheck: deviceCheck, tunnelRelay: selectorResult?.packetTunnelRelay @@ -98,17 +105,24 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { } override init() { + var loggerBuilder = LoggerBuilder() + let pid = ProcessInfo.processInfo.processIdentifier + loggerBuilder.metadata["pid"] = .string("\(pid)") - var metadata = Logger.Metadata() - metadata["pid"] = .string("\(pid)") + let bundleIdentifier = Bundle.main.bundleIdentifier! - initLoggingSystem( - bundleIdentifier: Bundle.main.bundleIdentifier!, - applicationGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier, - metadata: metadata + try? loggerBuilder.addFileOutput( + securityGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier, + basename: bundleIdentifier ) + #if DEBUG + loggerBuilder.addOSLogOutput(subsystem: bundleIdentifier) + #endif + + loggerBuilder.install() + providerLogger = Logger(label: "PacketTunnelProvider") tunnelLogger = Logger(label: "WireGuard") @@ -189,9 +203,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { message: "Failed to read tunnel configuration when starting the tunnel." ) - tunnelProviderError = .readConfiguration + configurationError = error startEmptyTunnel(completionHandler: completionHandler) + beginTunnelStartupFailureRecovery() return } @@ -228,34 +243,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { } } - private func startEmptyTunnel(completionHandler: @escaping (Error?) -> Void) { - let emptyTunnelConfiguration = TunnelConfiguration( - name: nil, - interface: InterfaceConfiguration(privateKey: PrivateKey()), - peers: [] - ) - - adapter.start(tunnelConfiguration: emptyTunnelConfiguration) { error in - self.dispatchQueue.async { - if let error = error { - self.providerLogger.error( - error: error, - message: "Failed to start an empty tunnel." - ) - - completionHandler(error) - } else { - self.providerLogger.debug("Started an empty tunnel.") - - self.startTunnelCompletionHandler = { [weak self] in - self?.isConnected = true - completionHandler(nil) - } - } - } - } - } - override func stopTunnel( with reason: NEProviderStopReason, completionHandler: @escaping () -> Void @@ -263,11 +250,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { providerLogger.debug("Stop the tunnel: \(reason)") dispatchQueue.async { + self.cancelTunnelStartupFailureRecovery() self.tunnelMonitor.stop() self.checkDeviceStateTask?.cancel() self.checkDeviceStateTask = nil self.startTunnelCompletionHandler = nil - self.reassertTunnelCompletionHandler = nil } adapter.stop { error in @@ -385,9 +372,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { startTunnelCompletionHandler?() startTunnelCompletionHandler = nil - reassertTunnelCompletionHandler?() - reassertTunnelCompletionHandler = nil - numberOfFailedAttempts = 0 checkDeviceStateTask?.cancel() @@ -413,7 +397,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { providerLogger.debug("Recover connection. Picking next relay...") - reconnectTunnel(to: .automatic, completionHandler: completionHandler) + reconnectTunnel(to: .automatic) { _ in + completionHandler() + } } func tunnelMonitor( @@ -432,6 +418,62 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { // MARK: - Private + private func beginTunnelStartupFailureRecovery() { + let timer = DispatchSource.makeTimerSource(queue: dispatchQueue) + timer.setEventHandler { [weak self] in + guard let self = self else { return } + + self.providerLogger.debug("Restart the tunnel that had startup failure.") + self.reconnectTunnel(to: .automatic) { [weak self] error in + if error == nil { + self?.cancelTunnelStartupFailureRecovery() + } + } + } + + timer.schedule( + wallDeadline: .now() + tunnelStartupFailureRestartInterval, + repeating: tunnelStartupFailureRestartInterval + ) + timer.activate() + + tunnelStartupFailureRecoveryTimer?.cancel() + tunnelStartupFailureRecoveryTimer = timer + } + + private func cancelTunnelStartupFailureRecovery() { + tunnelStartupFailureRecoveryTimer?.cancel() + tunnelStartupFailureRecoveryTimer = nil + } + + private func startEmptyTunnel(completionHandler: @escaping (Error?) -> Void) { + let emptyTunnelConfiguration = TunnelConfiguration( + name: nil, + interface: InterfaceConfiguration(privateKey: PrivateKey()), + peers: [] + ) + + adapter.start(tunnelConfiguration: emptyTunnelConfiguration) { error in + self.dispatchQueue.async { + if let error = error { + self.providerLogger.error( + error: error, + message: "Failed to start an empty tunnel." + ) + + completionHandler(error) + } else { + self.providerLogger.debug("Started an empty tunnel.") + + self.startTunnelCompletionHandler = { [weak self] in + self?.isConnected = true + completionHandler(nil) + } + } + } + } + } + private func setReconnecting(_ reconnecting: Bool) { // Raise reasserting flag, but only if tunnel has already moved to connected state once. // Otherwise keep the app in connecting state until it manages to establish the very first @@ -446,9 +488,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { { let tunnelSettings = try SettingsManager.readSettings() let deviceState = try SettingsManager.readDeviceState() - - tunnelProviderError = nil - let selectorResult: RelaySelectorResult switch nextRelay { @@ -467,19 +506,26 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { ) } - private func reconnectTunnel(to nextRelay: NextRelay, completionHandler: (() -> Void)? = nil) { + private func reconnectTunnel( + to nextRelay: NextRelay, + completionHandler: ((Error?) -> Void)? = nil + ) { dispatchPrecondition(condition: .onQueue(dispatchQueue)) // Read tunnel configuration. let tunnelConfiguration: PacketTunnelConfiguration do { tunnelConfiguration = try makeConfiguration(nextRelay) + configurationError = nil } catch { providerLogger.error( error: error, - message: "Failed produce new configuration." + message: "Failed to produce new configuration." ) - completionHandler?() + + configurationError = error + + completionHandler?(error) return } @@ -496,14 +542,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { adapter.update(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in self.dispatchQueue.async { if let error = error { - let wrappedError: PacketTunnelErrorWrapper = .wireguard( - error: error.localizedDescription - ) - - self.wgError = wrappedError - } - - if let error = error { + self.wgError = error self.providerLogger.error( error: error, message: "Failed to update WireGuard configuration." @@ -515,17 +554,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { self.providerLogger.debug( "Reset tunnel relay to \(oldSelectorResult?.relay.hostname ?? "none")." ) - self.reassertTunnelCompletionHandler = nil self.setReconnecting(false) - - completionHandler?() } else { - self.reassertTunnelCompletionHandler = completionHandler - self.tunnelMonitor.start( probeAddress: tunnelConfiguration.selectorResult.endpoint.ipv4Gateway ) } + completionHandler?(error) } } } @@ -712,3 +747,31 @@ extension DeviceCheck { } } } + +extension PacketTunnelErrorWrapper { + init?(error: Error) { + switch error { + case let error as WireGuardAdapterError: + self = .wireguard(error.localizedDescription) + + case is UnsupportedSettingsVersionError: + self = .configuration(.outdatedSchema) + + case let keychainError as KeychainError where keychainError == .interactionNotAllowed: + self = .configuration(.deviceLocked) + + case let error as ReadSettingsVersionError: + if case KeychainError.interactionNotAllowed = error.underlyingError as? KeychainError { + self = .configuration(.deviceLocked) + } else { + self = .configuration(.readFailure) + } + + case is NoRelaysSatisfyingConstraintsError: + self = .configuration(.noRelaysSatisfyingConstraints) + + default: + return nil + } + } +} |
