summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/MullvadLogging/LogFileOutputStream.swift168
-rw-r--r--ios/MullvadLogging/Logging.swift115
-rw-r--r--ios/MullvadLogging/TextFileOutputStream.swift87
-rw-r--r--ios/MullvadTypes/KeychainError.swift1
-rw-r--r--ios/MullvadTypes/PacketTunnelErrorWrapper.swift46
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj8
-rw-r--r--ios/MullvadVPN/AppDelegate.swift21
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift8
-rw-r--r--ios/MullvadVPN/ProblemReportReviewViewController.swift34
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift197
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
+ }
+ }
+}