summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-09-02 13:53:58 +0200
committerAndrej Mihajlov <and@mullvad.net>2020-09-02 13:53:58 +0200
commit41736337874f987a28ed08289d33a0c9ce87e45a (patch)
treedd82a653c5dcd11d8c4e5289652ee25d5c1cdd05
parent58ae8def1aba534b36dbb1e053debcb258c8d3e1 (diff)
parent0bddeb9cf8139eb0ed7a32c86fc25fe460feb0e4 (diff)
downloadmullvadvpn-41736337874f987a28ed08289d33a0c9ce87e45a.tar.xz
mullvadvpn-41736337874f987a28ed08289d33a0c9ce87e45a.zip
Merge branch 'fix-set-network-settings-callback'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj6
-rw-r--r--ios/MullvadVPN/AppDelegate.swift11
-rw-r--r--ios/MullvadVPN/ApplicationConfiguration.swift27
-rw-r--r--ios/MullvadVPN/AutomaticKeyRotationManager.swift42
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift4
-rw-r--r--ios/MullvadVPN/Operations/AsyncOperation.swift45
-rw-r--r--ios/MullvadVPN/Optional+DispatchQueue.swift22
-rw-r--r--ios/MullvadVPN/RelayCache.swift41
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift479
-rw-r--r--ios/PacketTunnel/WireguardDevice.swift96
10 files changed, 480 insertions, 293 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index cab698a459..f27bb0566a 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -61,6 +61,8 @@
582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */; };
582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B42295780F0055B6EF /* AccountExpiry.swift */; };
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; };
+ 583BC70724FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.swift */; };
+ 583BC70824FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.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 */; };
5840250422B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; };
@@ -288,6 +290,7 @@
582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountCell.swift; sourceTree = "<group>"; };
582BB1B42295780F0055B6EF /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; };
5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = "<group>"; };
+ 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+DispatchQueue.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>"; };
5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelIpc.swift; sourceTree = "<group>"; };
@@ -612,6 +615,7 @@
58C6B35322BB87C4003C19AD /* WireguardPrivateKey.swift */,
58F3C098249B978C003E76BE /* x25519.c */,
58F3C097249B978C003E76BE /* x25519.h */,
+ 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.swift */,
);
path = MullvadVPN;
sourceTree = "<group>";
@@ -1002,6 +1006,7 @@
580EE22124B3240100F9D8A1 /* TransformOperationObserver.swift in Sources */,
582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */,
5873884D239E6D7E00E96C4E /* EmbeddedViewContainerView.swift in Sources */,
+ 583BC70724FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */,
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */,
585FE2F124E1365400439C50 /* LogStreamer.swift in Sources */,
58B9EB132488ED2100095626 /* AlertPresenter.swift in Sources */,
@@ -1096,6 +1101,7 @@
581503A424D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */,
5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */,
5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */,
+ 583BC70824FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */,
58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */,
580EE21024B322E700F9D8A1 /* TransformOperation.swift in Sources */,
58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */,
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 2ee783dad0..d54ac731fc 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -21,9 +21,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let simulatorTunnelProvider = SimulatorTunnelProviderHost()
#endif
+ #if DEBUG
+ private let packetTunnelLogForwarder = LogStreamer<UTF8>(fileURLs: [ApplicationConfiguration.packetTunnelLogFileURL!])
+ #endif
+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
initLoggingSystem(bundleIdentifier: Bundle.main.bundleIdentifier!)
+ #if DEBUG
+ let stdoutStream = TextFileOutputStream.standardOutputStream()
+ packetTunnelLogForwarder.start { (str) in
+ stdoutStream.write("\(str)\n")
+ }
+ #endif
+
#if targetEnvironment(simulator)
SimulatorTunnelProvider.shared.delegate = simulatorTunnelProvider
#endif
diff --git a/ios/MullvadVPN/ApplicationConfiguration.swift b/ios/MullvadVPN/ApplicationConfiguration.swift
index 8dc5063fcb..1052df9851 100644
--- a/ios/MullvadVPN/ApplicationConfiguration.swift
+++ b/ios/MullvadVPN/ApplicationConfiguration.swift
@@ -16,16 +16,23 @@ class ApplicationConfiguration {
/// The application identifier for the PacketTunnel extension
static let packetTunnelExtensionIdentifier = "net.mullvad.MullvadVPN.PacketTunnel"
- /// The application log files
- static var logFileURLs: [URL] {
- let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Self.securityGroupIdentifier)
- let fileNames = ["net.mullvad.MullvadVPN", "net.mullvad.MullvadVPN.PacketTunnel"]
+ /// Container URL for security group
+ static var containerURL: URL? {
+ return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Self.securityGroupIdentifier)
+ }
+
+ /// The main application log file located in a shared container
+ static var mainApplicationLogFileURL: URL? {
+ return Self.containerURL?.appendingPathComponent("Logs/net.mullvad.MullvadVPN.log", isDirectory: false)
+ }
- return fileNames.compactMap { (fileName) -> URL? in
- return containerURL?
- .appendingPathComponent("Logs", isDirectory: true)
- .appendingPathComponent(fileName, isDirectory: false)
- .appendingPathExtension("log")
- }
+ /// The packet tunnel log file located in a shared container
+ static var packetTunnelLogFileURL: URL? {
+ return Self.containerURL?.appendingPathComponent("Logs/net.mullvad.MullvadVPN.PacketTunnel.log", isDirectory: false)
+ }
+
+ /// All log files located in a shared container
+ static var logFileURLs: [URL] {
+ return [mainApplicationLogFileURL, packetTunnelLogFileURL].compactMap { $0 }
}
}
diff --git a/ios/MullvadVPN/AutomaticKeyRotationManager.swift b/ios/MullvadVPN/AutomaticKeyRotationManager.swift
index 79e435a2a6..a6c7466ba8 100644
--- a/ios/MullvadVPN/AutomaticKeyRotationManager.swift
+++ b/ios/MullvadVPN/AutomaticKeyRotationManager.swift
@@ -69,6 +69,9 @@ class AutomaticKeyRotationManager {
/// A variable backing the `eventHandler` public property
private var _eventHandler: ((KeyRotationResult) -> Void)?
+ /// A dispatch queue used for broadcasting events
+ private let eventQueue: DispatchQueue?
+
/// An event handler that's invoked when key rotation occurred
var eventHandler: ((KeyRotationResult) -> Void)? {
get {
@@ -83,37 +86,38 @@ class AutomaticKeyRotationManager {
}
}
- init(persistentKeychainReference: Data) {
+ init(persistentKeychainReference: Data, eventQueue: DispatchQueue?) {
self.persistentKeychainReference = persistentKeychainReference
+ self.eventQueue = eventQueue
}
- func startAutomaticRotation(completionHandler: @escaping () -> Void) {
+ func startAutomaticRotation(queue: DispatchQueue?, completionHandler: @escaping () -> Void) {
dispatchQueue.async {
- guard !self.isAutomaticRotationEnabled else { return }
-
- self.logger.info("Start automatic key rotation")
+ if !self.isAutomaticRotationEnabled {
+ self.logger.info("Start automatic key rotation")
- self.isAutomaticRotationEnabled = true
- self.performKeyRotation()
+ self.isAutomaticRotationEnabled = true
+ self.performKeyRotation()
+ }
- completionHandler()
+ queue.performOnWrappedOrCurrentQueue(block: completionHandler)
}
}
- func stopAutomaticRotation(completionHandler: @escaping () -> Void) {
+ func stopAutomaticRotation(queue: DispatchQueue?, completionHandler: @escaping () -> Void) {
dispatchQueue.async {
- guard self.isAutomaticRotationEnabled else { return }
-
- self.logger.info("Stop automatic key rotation")
+ if self.isAutomaticRotationEnabled {
+ self.logger.info("Stop automatic key rotation")
- self.isAutomaticRotationEnabled = false
+ self.isAutomaticRotationEnabled = false
- self.dataTask?.cancel()
- self.dataTask = nil
+ self.dataTask?.cancel()
+ self.dataTask = nil
- self.timerSource?.cancel()
+ self.timerSource?.cancel()
+ }
- completionHandler()
+ queue.performOnWrappedOrCurrentQueue(block: completionHandler)
}
}
@@ -215,7 +219,9 @@ class AutomaticKeyRotationManager {
if event.isNew {
logger.info("Finished private key rotation")
- eventHandler?(event)
+ eventQueue.performOnWrappedOrCurrentQueue {
+ self.eventHandler?(event)
+ }
}
if let rotationDate = Self.nextRotation(creationDate: event.creationDate) {
diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift
index 5b36d20932..c46ee0270d 100644
--- a/ios/MullvadVPN/ConnectViewController.swift
+++ b/ios/MullvadVPN/ConnectViewController.swift
@@ -123,7 +123,7 @@ class ConnectViewController: UIViewController, RootContainment, TunnelObserver,
private func updateButtons() {
switch tunnelState {
- case .disconnected:
+ case .disconnected, .disconnecting:
selectLocationButton.setTitle(NSLocalizedString("Select location", comment: ""), for: .normal)
connectButton.setTitle(NSLocalizedString("Secure connection", comment: ""), for: .normal)
@@ -135,7 +135,7 @@ class ConnectViewController: UIViewController, RootContainment, TunnelObserver,
setArrangedButtons([selectLocationButton, splitDisconnectButtonView])
- case .connected, .reconnecting, .disconnecting:
+ case .connected, .reconnecting:
selectLocationButton.setTitle(NSLocalizedString("Switch location", comment: ""), for: .normal)
splitDisconnectButtonView.primaryButton.setTitle(NSLocalizedString("Disconnect", comment: ""), for: .normal)
diff --git a/ios/MullvadVPN/Operations/AsyncOperation.swift b/ios/MullvadVPN/Operations/AsyncOperation.swift
index 98a5f8aa39..7edd3bbdd4 100644
--- a/ios/MullvadVPN/Operations/AsyncOperation.swift
+++ b/ios/MullvadVPN/Operations/AsyncOperation.swift
@@ -11,9 +11,16 @@ import Foundation
/// A base implementation of an asynchronous operation
class AsyncOperation: Operation, OperationProtocol {
+ /// A state transaction lock used to perform critical sections of code within `start`, `cancel`
+ /// and `finish` calls.
+ fileprivate let transactionLock = NSRecursiveLock()
+
/// A state lock used for manipulating the operation state flags in a thread safe fashion.
fileprivate let stateLock = NSRecursiveLock()
+ /// The operation observers.
+ fileprivate var observers: [AnyOperationObserver<AsyncOperation>] = []
+
/// Operation state flags.
private var _isExecuting = false
private var _isFinished = false
@@ -36,8 +43,8 @@ class AsyncOperation: Operation, OperationProtocol {
}
final override func start() {
- stateLock.withCriticalBlock {
- if self._isCancelled {
+ transactionLock.withCriticalBlock {
+ if self.isCancelled {
self.finish()
} else {
self.setExecuting(true)
@@ -53,11 +60,17 @@ class AsyncOperation: Operation, OperationProtocol {
/// Cancel operation
/// Subclasses should override `operationDidCancel` instead
final override func cancel() {
- stateLock.withCriticalBlock {
- if !self._isCancelled {
+ transactionLock.withCriticalBlock {
+ if self.isCancelled {
+ super.cancel()
+ } else {
self.setCancelled(true)
- if self._isExecuting {
+ super.cancel()
+
+ // Only notify the operation about cancellation when it is already running,
+ // otherwise the call to `start` should automatically `finish()` the operation.
+ if self.isExecuting {
self.operationDidCancel()
}
}
@@ -71,17 +84,20 @@ class AsyncOperation: Operation, OperationProtocol {
}
final func finish() {
- stateLock.withCriticalBlock {
- if !self._isFinished {
+ transactionLock.withCriticalBlock {
+ guard !self.isFinished else { return }
+
+ self.stateLock.withCriticalBlock {
self.observers.forEach { $0.operationWillFinish(self) }
}
- if self._isExecuting {
+ if self.isExecuting {
self.setExecuting(false)
}
- if !self._isFinished {
- self.setFinished(true)
+ self.setFinished(true)
+
+ self.stateLock.withCriticalBlock {
self.observers.forEach { $0.operationDidFinish(self) }
}
}
@@ -89,27 +105,24 @@ class AsyncOperation: Operation, OperationProtocol {
private func setExecuting(_ value: Bool) {
willChangeValue(for: \.isExecuting)
- _isExecuting = value
+ stateLock.withCriticalBlock { _isExecuting = value }
didChangeValue(for: \.isExecuting)
}
private func setFinished(_ value: Bool) {
willChangeValue(for: \.isFinished)
- _isFinished = value
+ stateLock.withCriticalBlock { _isFinished = value }
didChangeValue(for: \.isFinished)
}
private func setCancelled(_ value: Bool) {
willChangeValue(for: \.isCancelled)
- _isCancelled = value
+ stateLock.withCriticalBlock { _isCancelled = value }
didChangeValue(for: \.isCancelled)
}
// MARK: - Observation
- /// The operation observers.
- fileprivate var observers: [AnyOperationObserver<AsyncOperation>] = []
-
/// Add type-erased operation observer
fileprivate func addAnyObserver(_ observer: AnyOperationObserver<AsyncOperation>) {
stateLock.withCriticalBlock {
diff --git a/ios/MullvadVPN/Optional+DispatchQueue.swift b/ios/MullvadVPN/Optional+DispatchQueue.swift
new file mode 100644
index 0000000000..34402977ec
--- /dev/null
+++ b/ios/MullvadVPN/Optional+DispatchQueue.swift
@@ -0,0 +1,22 @@
+//
+// Optional+DispatchQueue.swift
+// MullvadVPN
+//
+// Created by pronebird on 01/09/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension Optional where Wrapped == DispatchQueue {
+ /// Unwrap the `DispatchQueue` and perform the block on it, otherwise call the block
+ /// synchronously on the current queue when `Optional` is `.none`.
+ func performOnWrappedOrCurrentQueue(block: @escaping () -> Void) {
+ switch self {
+ case .some(let queue):
+ queue.async(execute: block)
+ case .none:
+ block()
+ }
+ }
+}
diff --git a/ios/MullvadVPN/RelayCache.swift b/ios/MullvadVPN/RelayCache.swift
index 802e278c14..c561303e49 100644
--- a/ios/MullvadVPN/RelayCache.swift
+++ b/ios/MullvadVPN/RelayCache.swift
@@ -110,35 +110,34 @@ class RelayCache {
self.cacheFileURL = cacheFileURL
}
- func startPeriodicUpdates(completionHandler: (() -> Void)?) {
+ func startPeriodicUpdates(queue: DispatchQueue?, completionHandler: (() -> Void)?) {
dispatchQueue.async {
- guard !self.isPeriodicUpdatesEnabled else {
- completionHandler?()
- return
- }
-
- self.isPeriodicUpdatesEnabled = true
+ if !self.isPeriodicUpdatesEnabled {
+ self.isPeriodicUpdatesEnabled = true
- switch Self.read(cacheFileURL: self.cacheFileURL) {
- case .success(let cachedRelayList):
- if let nextUpdate = Self.nextUpdateDate(lastUpdatedAt: cachedRelayList.updatedAt) {
- let startTime = Self.makeWalltime(fromDate: nextUpdate)
- self.scheduleRepeatingTimer(startTime: startTime)
- }
+ switch Self.read(cacheFileURL: self.cacheFileURL) {
+ case .success(let cachedRelayList):
+ if let nextUpdate = Self.nextUpdateDate(lastUpdatedAt: cachedRelayList.updatedAt) {
+ let startTime = Self.makeWalltime(fromDate: nextUpdate)
+ self.scheduleRepeatingTimer(startTime: startTime)
+ }
- case .failure(let readError):
- self.logger.error(chainedError: readError, message: "Failed to read the relay cache")
+ case .failure(let readError):
+ self.logger.error(chainedError: readError, message: "Failed to read the relay cache")
- if Self.shouldDownloadRelaysOnReadFailure(readError) {
- self.scheduleRepeatingTimer(startTime: .now())
+ if Self.shouldDownloadRelaysOnReadFailure(readError) {
+ self.scheduleRepeatingTimer(startTime: .now())
+ }
}
}
- completionHandler?()
+ queue.performOnWrappedOrCurrentQueue {
+ completionHandler?()
+ }
}
}
- func stopPeriodicUpdates(completionHandler: (() -> Void)?) {
+ func stopPeriodicUpdates(queue: DispatchQueue?, completionHandler: (() -> Void)?) {
dispatchQueue.async {
self.isPeriodicUpdatesEnabled = false
@@ -146,7 +145,9 @@ class RelayCache {
self.timerSource = nil
self.downloadTask?.cancel()
- completionHandler?()
+ queue.performOnWrappedOrCurrentQueue {
+ completionHandler?()
+ }
}
}
diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift
index eb07274955..784e3fc8fe 100644
--- a/ios/PacketTunnel/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider.swift
@@ -11,98 +11,6 @@ import Network
import NetworkExtension
import Logging
-enum PacketTunnelProviderError: ChainedError {
- /// Failure to read the relay cache
- case readRelayCache(RelayCacheError)
-
- /// Failure to satisfy the relay constraint
- case noRelaySatisfyingConstraint
-
- /// Missing the persistent keychain reference to the tunnel settings
- case missingKeychainConfigurationReference
-
- /// Failure to read the tunnel settings from Keychain
- case cannotReadTunnelSettings(TunnelSettingsManager.Error)
-
- /// Failure to set network settings
- case setNetworkSettings(Error)
-
- /// Failure to start the Wireguard backend
- case startWireguardDevice(WireguardDevice.Error)
-
- /// Failure to stop the Wireguard backend
- case stopWireguardDevice(WireguardDevice.Error)
-
- /// Failure to update the Wireguard configuration
- case updateWireguardConfiguration(Error)
-
- /// IPC handler failure
- case ipcHandler(PacketTunnelIpcHandler.Error)
-
- var errorDescription: String? {
- switch self {
- case .readRelayCache:
- return "Failure to read the relay cache"
-
- case .noRelaySatisfyingConstraint:
- return "No relay satisfying the given constraint"
-
- case .missingKeychainConfigurationReference:
- return "Invalid protocol configuration"
-
- case .cannotReadTunnelSettings:
- return "Failure to read tunnel settings"
-
- case .setNetworkSettings:
- return "Failure to set system network settings"
-
- case .startWireguardDevice:
- return "Failure to start the WireGuard device"
-
- case .stopWireguardDevice:
- return "Failure to stop the WireGuard device"
-
- case .updateWireguardConfiguration:
- return "Failure to update the Wireguard configuration"
-
- case .ipcHandler:
- return "Failure to handle the IPC request"
- }
- }
-}
-
-struct PacketTunnelConfiguration {
- var persistentKeychainReference: Data
- var tunnelSettings: TunnelSettings
- var selectorResult: RelaySelectorResult
-}
-
-extension PacketTunnelConfiguration {
- var wireguardConfig: WireguardConfiguration {
- let mullvadEndpoint = selectorResult.endpoint
- var peers: [AnyIPEndpoint] = [.ipv4(mullvadEndpoint.ipv4Relay)]
-
- if let ipv6Relay = mullvadEndpoint.ipv6Relay {
- peers.append(.ipv6(ipv6Relay))
- }
-
- let wireguardPeers = peers.map {
- WireguardPeer(
- endpoint: $0,
- publicKey: selectorResult.endpoint.publicKey)
- }
-
- return WireguardConfiguration(
- privateKey: tunnelSettings.interface.privateKey,
- peers: wireguardPeers,
- allowedIPs: [
- IPAddressRange(address: IPv4Address.any, networkPrefixLength: 0),
- IPAddressRange(address: IPv6Address.any, networkPrefixLength: 0)
- ]
- )
- }
-}
-
class PacketTunnelProvider: NEPacketTunnelProvider {
enum OperationCategory {
@@ -112,11 +20,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
/// Tunnel provider logger
private let logger: Logger
- /// Active wireguard device
- private var wireguardDevice: WireguardDevice?
-
- /// Active tunnel connection information
- private var connectionInfo: TunnelConnectionInfo?
+ /// Current tunnel state
+ private var tunnelState: PacketTunnelState = .disconnected {
+ didSet {
+ logger.info("New tunnel state: \(String(reflecting: self.tunnelState))")
+ }
+ }
/// Internal queue
private let dispatchQueue = DispatchQueue(label: "net.mullvad.MullvadVPN.PacketTunnel", qos: .utility)
@@ -131,13 +40,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return ExclusivityController(operationQueue: self.operationQueue)
}()
- private var keyRotationManager: AutomaticKeyRotationManager?
-
override init() {
initLoggingSystem(bundleIdentifier: Bundle.main.bundleIdentifier!)
- WireguardDevice.setTunnelLogger(Logger(label: "WireGuard"))
logger = Logger(label: "PacketTunnelProvider")
+
+ let wireguardLogger = Logger(label: "WireGuard")
+ WireguardDevice.setTunnelLogger(wireguardLogger)
}
// MARK: - Subclass
@@ -175,12 +84,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
case .failure(let error):
self.logger.error(chainedError: error, message: "Failed to stop the tunnel")
}
-
- completionHandler()
finish()
}
}
+ operation.addDidFinishBlockObserver { (op) in
+ completionHandler()
+ }
+
exclusivityController.addOperation(operation, categories: [.exclusive])
}
@@ -198,7 +109,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
case .tunnelInformation:
- self.replyAppMessage(.success(self.connectionInfo), completionHandler: completionHandler)
+ self.replyAppMessage(.success(self.tunnelState.tunnelConnectionInfo), completionHandler: completionHandler)
}
case .failure(let error):
@@ -219,50 +130,60 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// MARK: - Tunnel management
private func doStartTunnel(completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) {
+ self.tunnelState = .connecting(nil)
+
makePacketTunnelConfig { (result) in
guard case .success(let packetTunnelConfig) = result else {
+ self.tunnelState = .disconnected
+
completionHandler(result.map { _ in () })
return
}
+ self.tunnelState = .connecting(packetTunnelConfig.selectorResult.tunnelConnectionInfo)
+
self.updateNetworkSettings(packetTunnelConfig: packetTunnelConfig) { (result) in
guard case .success = result else {
+ self.tunnelState = .disconnected
+
completionHandler(result)
return
}
self.startWireguardDevice(packetFlow: self.packetFlow, configuration: packetTunnelConfig.wireguardConfig) { (result) in
- self.dispatchQueue.async {
- guard case .success(let device) = result else {
- completionHandler(result.map { _ in () })
- return
- }
+ guard case .success(let device) = result else {
+ self.tunnelState = .disconnected
+
+ completionHandler(result.map { _ in () })
+ return
+ }
- let persistentKeychainReference = packetTunnelConfig.persistentKeychainReference
- let keyRotationManager = AutomaticKeyRotationManager(persistentKeychainReference: persistentKeychainReference)
- keyRotationManager.eventHandler = { (keyRotationEvent) in
- self.dispatchQueue.async {
- self.reloadTunnelSettings { (result) in
- switch result {
- case .success:
- break
+ let persistentKeychainReference = packetTunnelConfig.persistentKeychainReference
+ let keyRotationManager = AutomaticKeyRotationManager(persistentKeychainReference: persistentKeychainReference, eventQueue: self.dispatchQueue)
+ keyRotationManager.eventHandler = { [weak self] (keyRotationEvent) in
+ guard let self = self else { return }
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to reload tunnel settings")
- }
- }
+ self.reloadTunnelSettings { (result) in
+ switch result {
+ case .success:
+ break
+
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to reload tunnel settings")
}
}
+ }
- self.wireguardDevice = device
- self.keyRotationManager = keyRotationManager
+ RelayCache.shared.startPeriodicUpdates(queue: self.dispatchQueue) {
+ keyRotationManager.startAutomaticRotation(queue: self.dispatchQueue) {
+ let context = PacketTunnelContext(
+ wireguardDevice: device,
+ keyRotationManager: keyRotationManager
+ )
- RelayCache.shared.startPeriodicUpdates {
- keyRotationManager.startAutomaticRotation {
- self.dispatchQueue.async {
- completionHandler(.success(()))
- }
- }
+ self.tunnelState = .connected(packetTunnelConfig.selectorResult.tunnelConnectionInfo, context)
+
+ completionHandler(.success(()))
}
}
}
@@ -271,49 +192,62 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
private func doStopTunnel(completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) {
- guard let device = self.wireguardDevice, let keyRotationManager = self.keyRotationManager
- else {
- completionHandler(.success(()))
- return
+ guard let context = self.tunnelState.context else {
+ logger.warning("Cannot stop the tunnel in such state: \(self.tunnelState)")
+ completionHandler(.failure(.invalidTunnelState))
+ return
}
- RelayCache.shared.stopPeriodicUpdates {
- keyRotationManager.stopAutomaticRotation {
- device.stop { (result) in
- self.dispatchQueue.async {
- self.wireguardDevice = nil
- self.keyRotationManager = nil
+ self.tunnelState = .disconnecting
- let result = result.mapError({ (error) -> PacketTunnelProviderError in
- return .stopWireguardDevice(error)
- })
- completionHandler(result)
- }
+ RelayCache.shared.stopPeriodicUpdates(queue: self.dispatchQueue) {
+ context.keyRotationManager.stopAutomaticRotation(queue: self.dispatchQueue) {
+ context.wireguardDevice.stop(queue: self.dispatchQueue) { (result) in
+ let result = result.mapError({ (error) -> PacketTunnelProviderError in
+ return .stopWireguardDevice(error)
+ })
+
+ self.tunnelState = .disconnected
+
+ completionHandler(result)
}
}
}
}
private func doReloadTunnelSettings(completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) {
- guard let device = self.wireguardDevice else {
- logger.warning("Ignore reloading tunnel settings. The WireguardDevice is not set yet.")
-
- completionHandler(.success(()))
+ guard let context = self.tunnelState.context else {
+ logger.warning("Cannot reload tunnel settings in such state: \(self.tunnelState)")
+ completionHandler(.failure(.invalidTunnelState))
return
}
logger.info("Reload tunnel settings")
+ let priorTunnelState = self.tunnelState
+ self.tunnelState = .reconnecting(nil, context)
+
makePacketTunnelConfig { (result) in
guard case .success(let packetTunnelConfig) = result else {
+ self.tunnelState = priorTunnelState
+
completionHandler(result.map { _ in () })
return
}
+ self.tunnelState = .reconnecting(packetTunnelConfig.selectorResult.tunnelConnectionInfo, context)
+
// Tell the system that the tunnel is about to reconnect with the new endpoint
self.reasserting = true
let finishReconnecting = { (result: Result<(), PacketTunnelProviderError>) in
+ switch result {
+ case .success:
+ self.tunnelState = .connected(packetTunnelConfig.selectorResult.tunnelConnectionInfo, context)
+ case .failure:
+ self.tunnelState = priorTunnelState
+ }
+
// Tell the system that the tunnel has finished reconnecting
self.reasserting = false
@@ -326,10 +260,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return
}
- device.setConfiguration(packetTunnelConfig.wireguardConfig) { (result) in
- self.dispatchQueue.async {
- finishReconnecting(result.mapError { PacketTunnelProviderError.updateWireguardConfiguration($0) })
- }
+ context.wireguardDevice.setConfiguration(packetTunnelConfig.wireguardConfig, queue: self.dispatchQueue) { (result) in
+ finishReconnecting(result.mapError { PacketTunnelProviderError.updateWireguardConfiguration($0) })
}
}
}
@@ -355,34 +287,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
}
- private func setTunnelConnectionInfo(selectorResult: RelaySelectorResult) {
- self.connectionInfo = TunnelConnectionInfo(
- ipv4Relay: selectorResult.endpoint.ipv4Relay,
- ipv6Relay: selectorResult.endpoint.ipv6Relay,
- hostname: selectorResult.relay.hostname,
- location: selectorResult.location
- )
-
- logger.info("Tunnel connection info: \(selectorResult.relay.hostname)")
- }
-
private func makePacketTunnelConfig(completionHandler: @escaping (Result<PacketTunnelConfiguration, PacketTunnelProviderError>) -> Void) {
guard let keychainReference = protocolConfiguration.passwordReference else {
completionHandler(.failure(.missingKeychainConfigurationReference))
return
}
- Self.makePacketTunnelConfig(keychainReference: keychainReference) { (result) in
- self.dispatchQueue.async {
- guard case .success(let packetTunnelConfig) = result else {
- completionHandler(result)
- return
- }
-
- self.setTunnelConnectionInfo(selectorResult: packetTunnelConfig.selectorResult)
-
- completionHandler(result)
- }
+ Self.makePacketTunnelConfig(keychainReference: keychainReference, queue: self.dispatchQueue) { (result) in
+ completionHandler(result)
}
}
@@ -410,21 +322,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
private func reloadTunnelSettings(completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) {
- let operation = ResultOperation<(), PacketTunnelProviderError> { (finish) in
+ let operation = AsyncBlockOperation { (finish) in
self.doReloadTunnelSettings { (result) in
- finish(result)
+ completionHandler(result)
+ finish()
}
}
- operation.addDidFinishBlockObserver(queue: dispatchQueue) { (operation, result) in
- completionHandler(result)
- }
-
exclusivityController.addOperation(operation, categories: [.exclusive])
}
/// Returns a `PacketTunnelConfig` that contains the tunnel settings and selected relay
- private class func makePacketTunnelConfig(keychainReference: Data, completionHandler: @escaping (Result<PacketTunnelConfiguration, PacketTunnelProviderError>) -> Void) {
+ private class func makePacketTunnelConfig(keychainReference: Data, queue: DispatchQueue?, completionHandler: @escaping (Result<PacketTunnelConfiguration, PacketTunnelProviderError>) -> Void) {
switch Self.readTunnelSettings(keychainReference: keychainReference) {
case .success(let tunnelSettings):
Self.selectRelayEndpoint(relayConstraints: tunnelSettings.relayConstraints) { (result) in
@@ -435,11 +344,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
selectorResult: selectorResult
)
}
- completionHandler(result)
+
+ queue.performOnWrappedOrCurrentQueue {
+ completionHandler(result)
+ }
}
case .failure(let error):
- completionHandler(.failure(error))
+ queue.performOnWrappedOrCurrentQueue {
+ completionHandler(.failure(error))
+ }
}
}
@@ -482,7 +396,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
logger.info("Tunnel interface is \(tunnelDeviceName)")
- device.start(configuration: configuration) { (result) in
+ device.start(queue: dispatchQueue, configuration: configuration) { (result) in
let result = result.map { device }
.mapError { PacketTunnelProviderError.startWireguardDevice($0) }
@@ -490,3 +404,202 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
}
}
+
+enum PacketTunnelProviderError: ChainedError {
+ /// Failure to perform operation in such state
+ case invalidTunnelState
+
+ /// Failure to read the relay cache
+ case readRelayCache(RelayCacheError)
+
+ /// Failure to satisfy the relay constraint
+ case noRelaySatisfyingConstraint
+
+ /// Missing the persistent keychain reference to the tunnel settings
+ case missingKeychainConfigurationReference
+
+ /// Failure to read the tunnel settings from Keychain
+ case cannotReadTunnelSettings(TunnelSettingsManager.Error)
+
+ /// Failure to set network settings
+ case setNetworkSettings(Error)
+
+ /// Failure to start the Wireguard backend
+ case startWireguardDevice(WireguardDevice.Error)
+
+ /// Failure to stop the Wireguard backend
+ case stopWireguardDevice(WireguardDevice.Error)
+
+ /// Failure to update the Wireguard configuration
+ case updateWireguardConfiguration(Error)
+
+ /// IPC handler failure
+ case ipcHandler(PacketTunnelIpcHandler.Error)
+
+ var errorDescription: String? {
+ switch self {
+ case .invalidTunnelState:
+ return "Failure to handle request in such tunnel state"
+
+ case .readRelayCache:
+ return "Failure to read the relay cache"
+
+ case .noRelaySatisfyingConstraint:
+ return "No relay satisfying the given constraint"
+
+ case .missingKeychainConfigurationReference:
+ return "Keychain configuration reference is not set on protocol configuration"
+
+ case .cannotReadTunnelSettings:
+ return "Failure to read tunnel settings"
+
+ case .setNetworkSettings:
+ return "Failure to set system network settings"
+
+ case .startWireguardDevice:
+ return "Failure to start the WireGuard device"
+
+ case .stopWireguardDevice:
+ return "Failure to stop the WireGuard device"
+
+ case .updateWireguardConfiguration:
+ return "Failure to update the Wireguard configuration"
+
+ case .ipcHandler:
+ return "Failure to handle the IPC request"
+ }
+ }
+}
+
+struct PacketTunnelConfiguration {
+ var persistentKeychainReference: Data
+ var tunnelSettings: TunnelSettings
+ var selectorResult: RelaySelectorResult
+}
+
+extension PacketTunnelConfiguration {
+ var wireguardConfig: WireguardConfiguration {
+ let mullvadEndpoint = selectorResult.endpoint
+ var peers: [AnyIPEndpoint] = [.ipv4(mullvadEndpoint.ipv4Relay)]
+
+ if let ipv6Relay = mullvadEndpoint.ipv6Relay {
+ peers.append(.ipv6(ipv6Relay))
+ }
+
+ let wireguardPeers = peers.map {
+ WireguardPeer(
+ endpoint: $0,
+ publicKey: selectorResult.endpoint.publicKey)
+ }
+
+ return WireguardConfiguration(
+ privateKey: tunnelSettings.interface.privateKey,
+ peers: wireguardPeers,
+ allowedIPs: [
+ IPAddressRange(address: IPv4Address.any, networkPrefixLength: 0),
+ IPAddressRange(address: IPv6Address.any, networkPrefixLength: 0)
+ ]
+ )
+ }
+}
+
+struct PacketTunnelContext {
+ let wireguardDevice: WireguardDevice
+ let keyRotationManager: AutomaticKeyRotationManager
+}
+
+enum PacketTunnelState {
+ case connecting(TunnelConnectionInfo?)
+ case connected(TunnelConnectionInfo, PacketTunnelContext)
+ case disconnecting
+ case disconnected
+ case reconnecting(TunnelConnectionInfo?, PacketTunnelContext)
+
+ var tunnelConnectionInfo: TunnelConnectionInfo? {
+ switch self {
+ case .connecting(let connectionInfo):
+ return connectionInfo
+ case .connected(let connectionInfo, _):
+ return connectionInfo
+ case .disconnecting:
+ return nil
+ case .disconnected:
+ return nil
+ case .reconnecting(let connectionInfo, _):
+ return connectionInfo
+ }
+ }
+
+ var context: PacketTunnelContext? {
+ switch self {
+ case .connecting:
+ return nil
+ case .connected(_, let context):
+ return context
+ case .disconnecting:
+ return nil
+ case .disconnected:
+ return nil
+ case .reconnecting(_, let context):
+ return context
+ }
+ }
+}
+
+extension PacketTunnelState: CustomStringConvertible, CustomDebugStringConvertible {
+ var description: String {
+ switch self {
+ case .connecting:
+ return "connecting"
+ case .connected:
+ return "connected"
+ case .disconnecting:
+ return "disconnecting"
+ case .disconnected:
+ return "disconnected"
+ case .reconnecting:
+ return "reconnecting"
+ }
+ }
+
+ var debugDescription: String {
+ var output = "PacketTunnelState."
+
+ switch self {
+ case .connecting(let connectionInfo):
+ output.append("connecting(")
+ output.append(String(reflecting: connectionInfo))
+ output.append(")")
+
+ case .connected(let connectionInfo, _):
+ output.append("connected(")
+ output.append(String(reflecting: connectionInfo))
+ output.append(")")
+
+ case .disconnecting:
+ output.append("disconnecting")
+
+ case .disconnected:
+ output.append("disconnected")
+
+ case .reconnecting(let connectionInfo, _):
+ output.append("reconnecting(")
+ output.append(String(reflecting: connectionInfo))
+ output.append(")")
+ }
+
+ return output
+ }
+}
+
+extension RelaySelectorResult {
+ var tunnelConnectionInfo: TunnelConnectionInfo {
+ return TunnelConnectionInfo(
+ ipv4Relay: self.endpoint.ipv4Relay,
+ ipv6Relay: self.endpoint.ipv6Relay,
+ hostname: self.relay.hostname,
+ location: self.location
+ )
+ }
+}
+
diff --git a/ios/PacketTunnel/WireguardDevice.swift b/ios/PacketTunnel/WireguardDevice.swift
index 5fcbe2e057..e45cf6bd4b 100644
--- a/ios/PacketTunnel/WireguardDevice.swift
+++ b/ios/PacketTunnel/WireguardDevice.swift
@@ -65,7 +65,7 @@ class WireguardDevice {
/// A private queue used for Wireguard logging
private static let loggingQueue = DispatchQueue(
label: "net.mullvad.vpn.packet-tunnel.wireguard-device.global-logging-queue",
- qos: .background
+ qos: .utility
)
/// A private queue used to synchronize access to `WireguardDevice` members
@@ -73,11 +73,6 @@ class WireguardDevice {
label: "net.mullvad.vpn.packet-tunnel.wireguard-device.work-queue"
)
- /// A private queue used for network monitor
- private let networkMonitorQueue = DispatchQueue(
- label: "net.mullvad.vpn.packet-tunnel.network-monitor"
- )
-
/// Network routes monitor
private var networkMonitor: NWPathMonitor?
@@ -141,14 +136,17 @@ class WireguardDevice {
deinit {
networkMonitor?.cancel()
+ stopWireguardBackend()
}
// MARK: - Public methods
- func start(configuration: WireguardConfiguration, completionHandler: @escaping (Result<(), Error>) -> Void) {
+ func start(queue: DispatchQueue?, configuration: WireguardConfiguration, completionHandler: @escaping (Result<(), Error>) -> Void) {
workQueue.async {
guard !self.isStarted else {
- completionHandler(.failure(.alreadyStarted))
+ queue.performOnWrappedOrCurrentQueue {
+ completionHandler(.failure(.alreadyStarted))
+ }
return
}
@@ -164,15 +162,19 @@ class WireguardDevice {
self.startNetworkMonitor()
- completionHandler(.success(()))
+ queue.performOnWrappedOrCurrentQueue {
+ completionHandler(.success(()))
+ }
case .failure(let error):
- completionHandler(.failure(error))
+ queue.performOnWrappedOrCurrentQueue {
+ completionHandler(.failure(error))
+ }
}
}
}
- func stop(completionHandler: @escaping (Result<(), Error>) -> Void) {
+ func stop(queue: DispatchQueue?, completionHandler: @escaping (Result<(), Error>) -> Void) {
workQueue.async {
if self.isStarted {
self.networkMonitor?.cancel()
@@ -181,14 +183,18 @@ class WireguardDevice {
self.stopWireguardBackend()
self.isStarted = false
- completionHandler(.success(()))
+ queue.performOnWrappedOrCurrentQueue {
+ completionHandler(.success(()))
+ }
} else {
- completionHandler(.failure(.notStarted))
+ queue.performOnWrappedOrCurrentQueue {
+ completionHandler(.failure(.notStarted))
+ }
}
}
}
- func setConfiguration(_ newConfiguration: WireguardConfiguration, completionHandler: @escaping (Result<(), Error>) -> Void) {
+ func setConfiguration(_ newConfiguration: WireguardConfiguration, queue: DispatchQueue?, completionHandler: @escaping (Result<(), Error>) -> Void) {
workQueue.async {
if self.isStarted {
if let handle = self.wireguardHandle {
@@ -200,9 +206,13 @@ class WireguardDevice {
self.configuration = newConfiguration
- completionHandler(.success(()))
+ queue.performOnWrappedOrCurrentQueue {
+ completionHandler(.success(()))
+ }
} else {
- completionHandler(.failure(.notStarted))
+ queue.performOnWrappedOrCurrentQueue {
+ completionHandler(.failure(.notStarted))
+ }
}
}
}
@@ -328,50 +338,48 @@ class WireguardDevice {
networkMonitor.pathUpdateHandler = { [weak self] (path) in
self?.didReceiveNetworkPathUpdate(path: path)
}
- networkMonitor.start(queue: networkMonitorQueue)
+ networkMonitor.start(queue: workQueue)
self.networkMonitor = networkMonitor
}
private func didReceiveNetworkPathUpdate(path: Network.NWPath) {
- workQueue.async {
- guard self.isStarted else { return }
+ guard self.isStarted else { return }
- self.logger.info("Network change detected. Status: \(path.status), interfaces \(path.availableInterfaces).")
+ self.logger.info("Network change detected. Status: \(path.status), interfaces \(path.availableInterfaces).")
- let oldPathSatisfied = self.isPathSatisfied
- let newPathSatisfied = path.status.isSatisfiable
+ let oldPathSatisfied = self.isPathSatisfied
+ let newPathSatisfied = path.status.isSatisfiable
- self.isPathSatisfied = newPathSatisfied
+ self.isPathSatisfied = newPathSatisfied
- switch (oldPathSatisfied, newPathSatisfied) {
- case (true, false):
- self.logger.info("Stop wireguard backend")
- self.stopWireguardBackend()
+ switch (oldPathSatisfied, newPathSatisfied) {
+ case (true, false):
+ self.logger.info("Stop wireguard backend")
+ self.stopWireguardBackend()
- case (false, true), (true, true):
- guard let currentConfiguration = self.configuration else { return }
+ case (false, true), (true, true):
+ guard let currentConfiguration = self.configuration else { return }
- self.logger.info("Re-resolve endpoints")
+ self.logger.info("Re-resolve endpoints")
- let resolvedConfiguration = self.resolveConfiguration(currentConfiguration)
+ let resolvedConfiguration = self.resolveConfiguration(currentConfiguration)
- if let handle = self.wireguardHandle {
- let commands = resolvedConfiguration.endpointUapiConfiguration()
- Self.setWireguardConfig(handle: handle, commands: commands)
+ if let handle = self.wireguardHandle {
+ let commands = resolvedConfiguration.endpointUapiConfiguration()
+ Self.setWireguardConfig(handle: handle, commands: commands)
- wgBumpSockets(handle)
- } else {
- self.logger.info("Start wireguard backend")
+ wgBumpSockets(handle)
+ } else {
+ self.logger.info("Start wireguard backend")
- if case .failure(let error) = self.startWireguardBackend(resolvedConfiguration: resolvedConfiguration) {
- self.logger.error(chainedError: error, message: "Failed to turn on WireGuard")
- }
+ if case .failure(let error) = self.startWireguardBackend(resolvedConfiguration: resolvedConfiguration) {
+ self.logger.error(chainedError: error, message: "Failed to turn on WireGuard")
}
-
- case (false, false):
- // No-op: device remains offline
- break
}
+
+ case (false, false):
+ // No-op: device remains offline
+ break
}
}
}