diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50cc5b5..3ab8fa3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
-- Manager package completely rewritten.
+- Manager package completely rewritten with Swift Concurrency.
- WireGuard: Use entities from WireGuardKit directly.
- Only enable on-demand if at least one rule is provided.
- Dropped incomplete support for IPSec/IKEv2.
diff --git a/Demo/Demo/DemoTunnel.plist b/Demo/Demo/DemoTunnel.plist
index bbfbc45..d2ac41c 100644
--- a/Demo/Demo/DemoTunnel.plist
+++ b/Demo/Demo/DemoTunnel.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
XPC!
CFBundleShortVersionString
- 4.0.0
+ 5.0.0
CFBundleVersion
1
LSMinimumSystemVersion
diff --git a/Demo/Demo/iOS/Demo.plist b/Demo/Demo/iOS/Demo.plist
index fa9f0c8..91387ee 100644
--- a/Demo/Demo/iOS/Demo.plist
+++ b/Demo/Demo/iOS/Demo.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 4.0.0
+ 5.0.0
CFBundleVersion
1
LSRequiresIPhoneOS
diff --git a/Demo/Demo/iOS/OpenVPNViewController.swift b/Demo/Demo/iOS/OpenVPNViewController.swift
index 5124616..f213fa2 100644
--- a/Demo/Demo/iOS/OpenVPNViewController.swift
+++ b/Demo/Demo/iOS/OpenVPNViewController.swift
@@ -125,17 +125,18 @@ class OpenVPNViewController: UIViewController {
var extra = NetworkExtensionExtra()
extra.passwordReference = passwordReference
-
vpn.reconnect(
tunnelIdentifier,
configuration: cfg!,
extra: extra,
- delay: nil
+ after: .seconds(2)
)
}
func disconnect() {
- vpn.disconnect()
+ Task {
+ await vpn.disconnect()
+ }
}
@IBAction func displayLog() {
diff --git a/Demo/Demo/iOS/WireGuardViewController.swift b/Demo/Demo/iOS/WireGuardViewController.swift
index 031ad31..831a0cc 100644
--- a/Demo/Demo/iOS/WireGuardViewController.swift
+++ b/Demo/Demo/iOS/WireGuardViewController.swift
@@ -72,7 +72,9 @@ class WireGuardViewController: UIViewController {
object: nil
)
- vpn.prepare()
+ Task {
+ await vpn.prepare()
+ }
}
@IBAction func connectionClicked(_ sender: Any) {
@@ -105,16 +107,20 @@ class WireGuardViewController: UIViewController {
return
}
- vpn.reconnect(
- tunnelIdentifier,
- configuration: cfg,
- extra: nil,
- delay: nil
- )
+ Task {
+ try await vpn.reconnect(
+ tunnelIdentifier,
+ configuration: cfg,
+ extra: nil,
+ after: .seconds(2)
+ )
+ }
}
func disconnect() {
- vpn.disconnect()
+ Task {
+ await vpn.disconnect()
+ }
}
func updateButton() {
diff --git a/Demo/Demo/macOS/Demo.plist b/Demo/Demo/macOS/Demo.plist
index b648ecc..82e7d7b 100644
--- a/Demo/Demo/macOS/Demo.plist
+++ b/Demo/Demo/macOS/Demo.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 4.0.0
+ 5.0.0
CFBundleVersion
1
LSApplicationCategoryType
diff --git a/Demo/Demo/macOS/OpenVPNViewController.swift b/Demo/Demo/macOS/OpenVPNViewController.swift
index fffadcb..93e4365 100644
--- a/Demo/Demo/macOS/OpenVPNViewController.swift
+++ b/Demo/Demo/macOS/OpenVPNViewController.swift
@@ -116,12 +116,11 @@ class OpenVPNViewController: NSViewController {
var extra = NetworkExtensionExtra()
extra.passwordReference = passwordReference
-
vpn.reconnect(
tunnelIdentifier,
configuration: cfg!,
extra: extra,
- delay: nil
+ after: .seconds(2)
)
}
diff --git a/Demo/Demo/macOS/WireGuardViewController.swift b/Demo/Demo/macOS/WireGuardViewController.swift
index 531c790..f6013b2 100644
--- a/Demo/Demo/macOS/WireGuardViewController.swift
+++ b/Demo/Demo/macOS/WireGuardViewController.swift
@@ -109,7 +109,7 @@ class WireGuardViewController: NSViewController {
tunnelIdentifier,
configuration: cfg,
extra: nil,
- delay: nil
+ after: .seconds(2)
)
}
diff --git a/Demo/Host/Info.plist b/Demo/Host/Info.plist
index fa9f0c8..91387ee 100644
--- a/Demo/Host/Info.plist
+++ b/Demo/Host/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 4.0.0
+ 5.0.0
CFBundleVersion
1
LSRequiresIPhoneOS
diff --git a/Demo/TunnelKit.xcodeproj/project.pbxproj b/Demo/TunnelKit.xcodeproj/project.pbxproj
index 301bdf3..65fcdf2 100644
--- a/Demo/TunnelKit.xcodeproj/project.pbxproj
+++ b/Demo/TunnelKit.xcodeproj/project.pbxproj
@@ -1116,8 +1116,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
- MACOSX_DEPLOYMENT_TARGET = 10.14;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
PATH = "${PATH}:/opt/homebrew/bin";
@@ -1177,8 +1177,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
- MACOSX_DEPLOYMENT_TARGET = 10.14;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
PATH = "${PATH}:/opt/homebrew/bin";
SDKROOT = iphoneos;
diff --git a/Package.swift b/Package.swift
index 63121da..573336b 100644
--- a/Package.swift
+++ b/Package.swift
@@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "TunnelKit",
platforms: [
- .iOS(.v12), .macOS(.v10_14)
+ .iOS(.v13), .macOS(.v10_15)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
diff --git a/Sources/TunnelKitManager/MockVPN.swift b/Sources/TunnelKitManager/MockVPN.swift
index 93b35df..baad68d 100644
--- a/Sources/TunnelKitManager/MockVPN.swift
+++ b/Sources/TunnelKitManager/MockVPN.swift
@@ -38,13 +38,21 @@ public class MockVPN: VPN {
notifyStatus(.disconnected)
}
- public func install(_ tunnelBundleIdentifier: String, configuration: NetworkExtensionConfiguration, extra: Data?, completionHandler: ((Result) -> Void)?) {
+ public func install(
+ _ tunnelBundleIdentifier: String,
+ configuration: NetworkExtensionConfiguration,
+ extra: Data?
+ ) {
notifyReinstall(true)
notifyStatus(.disconnected)
- completionHandler?(.success(()))
}
- public func reconnect(_ tunnelBundleIdentifier: String, configuration: NetworkExtensionConfiguration, extra: Data?, delay: Double?) {
+ public func reconnect(
+ _ tunnelBundleIdentifier: String,
+ configuration: NetworkExtensionConfiguration,
+ extra: Data?,
+ after: DispatchTimeInterval
+ ) {
notifyReinstall(true)
notifyStatus(.connected)
}
diff --git a/Sources/TunnelKitManager/NetworkExtensionVPN.swift b/Sources/TunnelKitManager/NetworkExtensionVPN.swift
index ece0219..18cf665 100644
--- a/Sources/TunnelKitManager/NetworkExtensionVPN.swift
+++ b/Sources/TunnelKitManager/NetworkExtensionVPN.swift
@@ -31,6 +31,7 @@ private let log = SwiftyBeaver.self
/// `VPN` based on the NetworkExtension framework.
public class NetworkExtensionVPN: VPN {
+ private let semaphore = DispatchSemaphore(value: 1)
/**
Initializes a provider.
@@ -44,172 +45,204 @@ public class NetworkExtensionVPN: VPN {
deinit {
NotificationCenter.default.removeObserver(self)
}
+
+ // MARK: Public
- // MARK: VPN
-
- public func prepare() {
- NETunnelProviderManager.loadAllFromPreferences { managers, error in
+ public func prepare() async {
+ await withCheckedContinuation { (continuation: CheckedContinuation) in
+ NETunnelProviderManager.loadAllFromPreferences { managers, error in
+ continuation.resume()
+ }
}
}
-
+
public func install(
_ tunnelBundleIdentifier: String,
configuration: NetworkExtensionConfiguration,
- extra: NetworkExtensionExtra?,
- completionHandler: ((Result) -> Void)?
- ) {
- let proto: NETunnelProviderProtocol
- do {
- proto = try configuration.asTunnelProtocol(
- withBundleIdentifier: tunnelBundleIdentifier,
- extra: extra
- )
- } catch {
- completionHandler?(.failure(error))
- return
- }
- lookupAll { result in
- switch result {
- case .success(let managers):
-
- // install (new or existing) then callback
- let targetManager = managers.first {
- $0.isTunnel(withIdentifier: tunnelBundleIdentifier)
- } ?? NETunnelProviderManager()
-
- self.install(
- targetManager,
- title: configuration.title,
- protocolConfiguration: proto,
- onDemandRules: extra?.onDemandRules ?? [],
- completionHandler: completionHandler
- )
-
- // remove others afterwards (to avoid permission request)
- managers.filter {
- !$0.isTunnel(withIdentifier: tunnelBundleIdentifier)
- }.forEach {
- $0.removeFromPreferences(completionHandler: nil)
- }
-
- case .failure(let error):
- completionHandler?(.failure(error))
- self.notifyError(error)
- }
- }
- }
-
- public func reconnect(
- _ tunnelBundleIdentifier: String,
- configuration: NetworkExtensionConfiguration,
- extra: Extra?,
- delay: Double?
- ) {
- let delay = delay ?? 2.0
- install(
+ extra: NetworkExtensionExtra?
+ ) async throws {
+ _ = try await installReturningManager(
tunnelBundleIdentifier,
configuration: configuration,
extra: extra
- ) { result in
- switch result {
- case .success(let manager):
- if manager.connection.status != .disconnected {
- manager.connection.stopVPNTunnel()
- DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
- self.connect(manager)
- }
- } else {
- self.connect(manager)
- }
+ )
+ }
- case .failure(let error):
- self.notifyError(error)
+ public func reconnect(
+ _ tunnelBundleIdentifier: String,
+ configuration: NetworkExtensionConfiguration,
+ extra: NetworkExtensionExtra?,
+ after: DispatchTimeInterval
+ ) async throws {
+ do {
+ let manager = try await installReturningManager(
+ tunnelBundleIdentifier,
+ configuration: configuration,
+ extra: extra
+ )
+ if manager.connection.status != .disconnected {
+ manager.connection.stopVPNTunnel()
+ try await Task.sleep(nanoseconds: after.nanoseconds)
}
+ try manager.connection.startVPNTunnel()
+ } catch {
+ notifyError(error)
+ throw error
}
}
- public func disconnect() {
- lookupAll {
- if case .success(let managers) = $0 {
+ public func disconnect() async {
+ do {
+ let managers = try await lookupAll()
+ await withCheckedContinuation { (continuation: CheckedContinuation) in
+ guard !managers.isEmpty else {
+ continuation.resume()
+ return
+ }
managers.forEach {
+ let isLast = ($0 == managers.last)
$0.connection.stopVPNTunnel()
$0.isOnDemandEnabled = false
$0.isEnabled = false
- $0.saveToPreferences(completionHandler: nil)
+ $0.saveToPreferences { _ in
+ if isLast {
+ continuation.resume()
+ }
+ }
}
}
+ } catch {
}
}
- public func uninstall() {
- lookupAll {
- if case .success(let managers) = $0 {
+ public func uninstall() async {
+ do {
+ let managers = try await lookupAll()
+ await withCheckedContinuation { (continuation: CheckedContinuation) in
+ guard !managers.isEmpty else {
+ continuation.resume()
+ return
+ }
managers.forEach {
+ let isLast = ($0 == managers.last)
$0.connection.stopVPNTunnel()
- $0.removeFromPreferences(completionHandler: nil)
+ $0.removeFromPreferences { _ in
+ if isLast {
+ continuation.resume()
+ }
+ }
}
}
+ } catch {
}
}
// MARK: Helpers
+
+ @discardableResult
+ private func installReturningManager(
+ _ tunnelBundleIdentifier: String,
+ configuration: NetworkExtensionConfiguration,
+ extra: NetworkExtensionExtra?
+ ) async throws -> NETunnelProviderManager {
+ let proto = try configuration.asTunnelProtocol(
+ withBundleIdentifier: tunnelBundleIdentifier,
+ extra: extra
+ )
+ let managers = try await lookupAll()
+
+ // install (new or existing) then callback
+ let targetManager = managers.first {
+ $0.isTunnel(withIdentifier: tunnelBundleIdentifier)
+ } ?? NETunnelProviderManager()
+
+ _ = try await install(
+ targetManager,
+ title: configuration.title,
+ protocolConfiguration: proto,
+ onDemandRules: extra?.onDemandRules ?? []
+ )
+
+ // remove others afterwards (to avoid permission request)
+ await retainManagers(managers) {
+ $0.isTunnel(withIdentifier: tunnelBundleIdentifier)
+ }
+
+ return targetManager
+ }
+ @discardableResult
private func install(
_ manager: NETunnelProviderManager,
title: String,
protocolConfiguration: NETunnelProviderProtocol,
- onDemandRules: [NEOnDemandRule],
- completionHandler: ((Result) -> Void)?
- ) {
- manager.localizedDescription = title
- manager.protocolConfiguration = protocolConfiguration
+ onDemandRules: [NEOnDemandRule]
+ ) async throws -> NETunnelProviderManager {
+ try await withCheckedThrowingContinuation { continuation in
+ manager.localizedDescription = title
+ manager.protocolConfiguration = protocolConfiguration
- if !onDemandRules.isEmpty {
- manager.onDemandRules = onDemandRules
- manager.isOnDemandEnabled = true
- } else {
- manager.isOnDemandEnabled = false
- }
-
- manager.isEnabled = true
- manager.saveToPreferences { error in
- if let error = error {
+ if !onDemandRules.isEmpty {
+ manager.onDemandRules = onDemandRules
+ manager.isOnDemandEnabled = true
+ } else {
manager.isOnDemandEnabled = false
- manager.isEnabled = false
- completionHandler?(.failure(error))
- self.notifyError(error)
- return
}
- manager.loadFromPreferences { error in
+
+ manager.isEnabled = true
+ manager.saveToPreferences { error in
if let error = error {
- completionHandler?(.failure(error))
+ manager.isOnDemandEnabled = false
+ manager.isEnabled = false
+ continuation.resume(throwing: error)
self.notifyError(error)
- return
+ } else {
+ manager.loadFromPreferences { error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ self.notifyError(error)
+ } else {
+ continuation.resume(returning: manager)
+ self.notifyReinstall(manager)
+ }
+ }
}
- completionHandler?(.success(manager))
- self.notifyReinstall(manager)
}
}
}
- private func connect(_ manager: NETunnelProviderManager) {
- do {
- try manager.connection.startVPNTunnel()
- } catch {
- notifyError(error)
+ private func retainManagers(_ managers: [NETunnelProviderManager], isIncluded: (NETunnelProviderManager) -> Bool) async {
+ await withCheckedContinuation { (continuation: CheckedContinuation) in
+ let others = managers.filter {
+ !isIncluded($0)
+ }
+ guard !others.isEmpty else {
+ continuation.resume()
+ return
+ }
+ others.forEach {
+ let isLast = ($0 == others.last)
+ $0.removeFromPreferences { _ in
+ if isLast {
+ continuation.resume()
+ }
+ }
+ }
}
}
- public func lookupAll(completionHandler: @escaping (Result<[NETunnelProviderManager], Error>) -> Void) {
- NETunnelProviderManager.loadAllFromPreferences { managers, error in
- if let error = error {
- completionHandler(.failure(error))
- return
+ private func lookupAll() async throws -> [NETunnelProviderManager] {
+ try await withCheckedThrowingContinuation { continuation in
+ NETunnelProviderManager.loadAllFromPreferences { managers, error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume(returning: managers ?? [])
+ }
}
- completionHandler(.success(managers ?? []))
}
}
-
+
// MARK: Notifications
@objc private func vpnDidUpdate(_ notification: Notification) {
diff --git a/Sources/TunnelKitManager/VPN.swift b/Sources/TunnelKitManager/VPN.swift
index fcf8a49..60e477e 100644
--- a/Sources/TunnelKitManager/VPN.swift
+++ b/Sources/TunnelKitManager/VPN.swift
@@ -27,8 +27,6 @@ import Foundation
/// Helps controlling a VPN without messing with underlying implementations.
public protocol VPN {
- associatedtype Manager
-
associatedtype Configuration
associatedtype Extra
@@ -36,7 +34,7 @@ public protocol VPN {
/**
Synchronizes with the current VPN state.
*/
- func prepare()
+ func prepare() async
/**
Installs the VPN profile.
@@ -44,14 +42,12 @@ public protocol VPN {
- Parameter tunnelBundleIdentifier: The bundle identifier of the tunnel extension.
- Parameter configuration: The configuration to install.
- Parameter extra: Optional extra arguments.
- - Parameter completionHandler: The completion handler.
*/
func install(
_ tunnelBundleIdentifier: String,
configuration: Configuration,
- extra: Extra?,
- completionHandler: ((Result) -> Void)?
- )
+ extra: Extra?
+ ) async throws
/**
Reconnects to the VPN.
@@ -59,22 +55,46 @@ public protocol VPN {
- Parameter tunnelBundleIdentifier: The bundle identifier of the tunnel extension.
- Parameter configuration: The configuration to install.
- Parameter extra: Optional extra arguments.
- - Parameter delay: The reconnection delay in seconds.
+ - Parameter after: The reconnection delay.
*/
func reconnect(
_ tunnelBundleIdentifier: String,
configuration: Configuration,
extra: Extra?,
- delay: Double?
- )
+ after: DispatchTimeInterval
+ ) async throws
/**
Disconnects from the VPN.
*/
- func disconnect()
+ func disconnect() async
/**
Uninstalls the VPN profile.
*/
- func uninstall()
+ func uninstall() async
+}
+
+extension DispatchTimeInterval {
+ public var nanoseconds: UInt64 {
+ switch self {
+ case .seconds(let sec):
+ return UInt64(sec) * NSEC_PER_SEC
+
+ case .milliseconds(let msec):
+ return UInt64(msec) * NSEC_PER_MSEC
+
+ case .microseconds(let usec):
+ return UInt64(usec) * NSEC_PER_USEC
+
+ case .nanoseconds(let nsec):
+ return UInt64(nsec)
+
+ case .never:
+ return 0
+
+ @unknown default:
+ return 0
+ }
+ }
}