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 + } + } }