Make VPN public methods async

- With Swift Concurrency
- Raise targets to iOS 13 / macOS 10.15
This commit is contained in:
Davide De Rosa 2022-04-04 02:50:43 +02:00
parent 990a0b85a6
commit e12e0b3051
14 changed files with 220 additions and 153 deletions

View File

@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Manager package completely rewritten. - Manager package completely rewritten with Swift Concurrency.
- WireGuard: Use entities from WireGuardKit directly. - WireGuard: Use entities from WireGuardKit directly.
- Only enable on-demand if at least one rule is provided. - Only enable on-demand if at least one rule is provided.
- Dropped incomplete support for IPSec/IKEv2. - Dropped incomplete support for IPSec/IKEv2.

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XPC!</string> <string>XPC!</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>4.0.0</string> <string>5.0.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>

View File

@ -15,7 +15,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>4.0.0</string> <string>5.0.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>

View File

@ -125,17 +125,18 @@ class OpenVPNViewController: UIViewController {
var extra = NetworkExtensionExtra() var extra = NetworkExtensionExtra()
extra.passwordReference = passwordReference extra.passwordReference = passwordReference
vpn.reconnect( vpn.reconnect(
tunnelIdentifier, tunnelIdentifier,
configuration: cfg!, configuration: cfg!,
extra: extra, extra: extra,
delay: nil after: .seconds(2)
) )
} }
func disconnect() { func disconnect() {
vpn.disconnect() Task {
await vpn.disconnect()
}
} }
@IBAction func displayLog() { @IBAction func displayLog() {

View File

@ -72,7 +72,9 @@ class WireGuardViewController: UIViewController {
object: nil object: nil
) )
vpn.prepare() Task {
await vpn.prepare()
}
} }
@IBAction func connectionClicked(_ sender: Any) { @IBAction func connectionClicked(_ sender: Any) {
@ -105,16 +107,20 @@ class WireGuardViewController: UIViewController {
return return
} }
vpn.reconnect( Task {
tunnelIdentifier, try await vpn.reconnect(
configuration: cfg, tunnelIdentifier,
extra: nil, configuration: cfg,
delay: nil extra: nil,
) after: .seconds(2)
)
}
} }
func disconnect() { func disconnect() {
vpn.disconnect() Task {
await vpn.disconnect()
}
} }
func updateButton() { func updateButton() {

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>4.0.0</string> <string>5.0.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>

View File

@ -116,12 +116,11 @@ class OpenVPNViewController: NSViewController {
var extra = NetworkExtensionExtra() var extra = NetworkExtensionExtra()
extra.passwordReference = passwordReference extra.passwordReference = passwordReference
vpn.reconnect( vpn.reconnect(
tunnelIdentifier, tunnelIdentifier,
configuration: cfg!, configuration: cfg!,
extra: extra, extra: extra,
delay: nil after: .seconds(2)
) )
} }

View File

@ -109,7 +109,7 @@ class WireGuardViewController: NSViewController {
tunnelIdentifier, tunnelIdentifier,
configuration: cfg, configuration: cfg,
extra: nil, extra: nil,
delay: nil after: .seconds(2)
) )
} }

View File

@ -15,7 +15,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>4.0.0</string> <string>5.0.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>

View File

@ -1116,8 +1116,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
PATH = "${PATH}:/opt/homebrew/bin"; PATH = "${PATH}:/opt/homebrew/bin";
@ -1177,8 +1177,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
PATH = "${PATH}:/opt/homebrew/bin"; PATH = "${PATH}:/opt/homebrew/bin";
SDKROOT = iphoneos; SDKROOT = iphoneos;

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "TunnelKit", name: "TunnelKit",
platforms: [ platforms: [
.iOS(.v12), .macOS(.v10_14) .iOS(.v13), .macOS(.v10_15)
], ],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -38,13 +38,21 @@ public class MockVPN: VPN {
notifyStatus(.disconnected) notifyStatus(.disconnected)
} }
public func install(_ tunnelBundleIdentifier: String, configuration: NetworkExtensionConfiguration, extra: Data?, completionHandler: ((Result<Void, Error>) -> Void)?) { public func install(
_ tunnelBundleIdentifier: String,
configuration: NetworkExtensionConfiguration,
extra: Data?
) {
notifyReinstall(true) notifyReinstall(true)
notifyStatus(.disconnected) 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) notifyReinstall(true)
notifyStatus(.connected) notifyStatus(.connected)
} }

View File

@ -31,6 +31,7 @@ private let log = SwiftyBeaver.self
/// `VPN` based on the NetworkExtension framework. /// `VPN` based on the NetworkExtension framework.
public class NetworkExtensionVPN: VPN { public class NetworkExtensionVPN: VPN {
private let semaphore = DispatchSemaphore(value: 1)
/** /**
Initializes a provider. Initializes a provider.
@ -45,168 +46,200 @@ public class NetworkExtensionVPN: VPN {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
} }
// MARK: VPN // MARK: Public
public func prepare() { public func prepare() async {
NETunnelProviderManager.loadAllFromPreferences { managers, error in await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
NETunnelProviderManager.loadAllFromPreferences { managers, error in
continuation.resume()
}
} }
} }
public func install( public func install(
_ tunnelBundleIdentifier: String, _ tunnelBundleIdentifier: String,
configuration: NetworkExtensionConfiguration, configuration: NetworkExtensionConfiguration,
extra: NetworkExtensionExtra?, extra: NetworkExtensionExtra?
completionHandler: ((Result<NETunnelProviderManager, Error>) -> Void)? ) async throws {
) { _ = try await installReturningManager(
let proto: NETunnelProviderProtocol tunnelBundleIdentifier,
do { configuration: configuration,
proto = try configuration.asTunnelProtocol( extra: extra
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( public func reconnect(
_ tunnelBundleIdentifier: String, _ tunnelBundleIdentifier: String,
configuration: NetworkExtensionConfiguration, configuration: NetworkExtensionConfiguration,
extra: Extra?, extra: NetworkExtensionExtra?,
delay: Double? after: DispatchTimeInterval
) { ) async throws {
let delay = delay ?? 2.0 do {
install( let manager = try await installReturningManager(
tunnelBundleIdentifier, tunnelBundleIdentifier,
configuration: configuration, configuration: configuration,
extra: extra extra: extra
) { result in )
switch result { if manager.connection.status != .disconnected {
case .success(let manager): manager.connection.stopVPNTunnel()
if manager.connection.status != .disconnected { try await Task.sleep(nanoseconds: after.nanoseconds)
manager.connection.stopVPNTunnel()
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.connect(manager)
}
} else {
self.connect(manager)
}
case .failure(let error):
self.notifyError(error)
} }
try manager.connection.startVPNTunnel()
} catch {
notifyError(error)
throw error
} }
} }
public func disconnect() { public func disconnect() async {
lookupAll { do {
if case .success(let managers) = $0 { let managers = try await lookupAll()
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
guard !managers.isEmpty else {
continuation.resume()
return
}
managers.forEach { managers.forEach {
let isLast = ($0 == managers.last)
$0.connection.stopVPNTunnel() $0.connection.stopVPNTunnel()
$0.isOnDemandEnabled = false $0.isOnDemandEnabled = false
$0.isEnabled = false $0.isEnabled = false
$0.saveToPreferences(completionHandler: nil) $0.saveToPreferences { _ in
if isLast {
continuation.resume()
}
}
} }
} }
} catch {
} }
} }
public func uninstall() { public func uninstall() async {
lookupAll { do {
if case .success(let managers) = $0 { let managers = try await lookupAll()
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
guard !managers.isEmpty else {
continuation.resume()
return
}
managers.forEach { managers.forEach {
let isLast = ($0 == managers.last)
$0.connection.stopVPNTunnel() $0.connection.stopVPNTunnel()
$0.removeFromPreferences(completionHandler: nil) $0.removeFromPreferences { _ in
if isLast {
continuation.resume()
}
}
} }
} }
} catch {
} }
} }
// MARK: Helpers // 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( private func install(
_ manager: NETunnelProviderManager, _ manager: NETunnelProviderManager,
title: String, title: String,
protocolConfiguration: NETunnelProviderProtocol, protocolConfiguration: NETunnelProviderProtocol,
onDemandRules: [NEOnDemandRule], onDemandRules: [NEOnDemandRule]
completionHandler: ((Result<NETunnelProviderManager, Error>) -> Void)? ) async throws -> NETunnelProviderManager {
) { try await withCheckedThrowingContinuation { continuation in
manager.localizedDescription = title manager.localizedDescription = title
manager.protocolConfiguration = protocolConfiguration manager.protocolConfiguration = protocolConfiguration
if !onDemandRules.isEmpty { if !onDemandRules.isEmpty {
manager.onDemandRules = onDemandRules manager.onDemandRules = onDemandRules
manager.isOnDemandEnabled = true manager.isOnDemandEnabled = true
} else { } else {
manager.isOnDemandEnabled = false
}
manager.isEnabled = true
manager.saveToPreferences { error in
if let error = error {
manager.isOnDemandEnabled = false 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 { if let error = error {
completionHandler?(.failure(error)) manager.isOnDemandEnabled = false
manager.isEnabled = false
continuation.resume(throwing: error)
self.notifyError(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) { private func retainManagers(_ managers: [NETunnelProviderManager], isIncluded: (NETunnelProviderManager) -> Bool) async {
do { await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
try manager.connection.startVPNTunnel() let others = managers.filter {
} catch { !isIncluded($0)
notifyError(error) }
} guard !others.isEmpty else {
} continuation.resume()
public func lookupAll(completionHandler: @escaping (Result<[NETunnelProviderManager], Error>) -> Void) {
NETunnelProviderManager.loadAllFromPreferences { managers, error in
if let error = error {
completionHandler(.failure(error))
return return
} }
completionHandler(.success(managers ?? [])) others.forEach {
let isLast = ($0 == others.last)
$0.removeFromPreferences { _ in
if isLast {
continuation.resume()
}
}
}
}
}
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 ?? [])
}
}
} }
} }

View File

@ -27,8 +27,6 @@ import Foundation
/// Helps controlling a VPN without messing with underlying implementations. /// Helps controlling a VPN without messing with underlying implementations.
public protocol VPN { public protocol VPN {
associatedtype Manager
associatedtype Configuration associatedtype Configuration
associatedtype Extra associatedtype Extra
@ -36,7 +34,7 @@ public protocol VPN {
/** /**
Synchronizes with the current VPN state. Synchronizes with the current VPN state.
*/ */
func prepare() func prepare() async
/** /**
Installs the VPN profile. Installs the VPN profile.
@ -44,14 +42,12 @@ public protocol VPN {
- Parameter tunnelBundleIdentifier: The bundle identifier of the tunnel extension. - Parameter tunnelBundleIdentifier: The bundle identifier of the tunnel extension.
- Parameter configuration: The configuration to install. - Parameter configuration: The configuration to install.
- Parameter extra: Optional extra arguments. - Parameter extra: Optional extra arguments.
- Parameter completionHandler: The completion handler.
*/ */
func install( func install(
_ tunnelBundleIdentifier: String, _ tunnelBundleIdentifier: String,
configuration: Configuration, configuration: Configuration,
extra: Extra?, extra: Extra?
completionHandler: ((Result<Manager, Error>) -> Void)? ) async throws
)
/** /**
Reconnects to the VPN. Reconnects to the VPN.
@ -59,22 +55,46 @@ public protocol VPN {
- Parameter tunnelBundleIdentifier: The bundle identifier of the tunnel extension. - Parameter tunnelBundleIdentifier: The bundle identifier of the tunnel extension.
- Parameter configuration: The configuration to install. - Parameter configuration: The configuration to install.
- Parameter extra: Optional extra arguments. - Parameter extra: Optional extra arguments.
- Parameter delay: The reconnection delay in seconds. - Parameter after: The reconnection delay.
*/ */
func reconnect( func reconnect(
_ tunnelBundleIdentifier: String, _ tunnelBundleIdentifier: String,
configuration: Configuration, configuration: Configuration,
extra: Extra?, extra: Extra?,
delay: Double? after: DispatchTimeInterval
) ) async throws
/** /**
Disconnects from the VPN. Disconnects from the VPN.
*/ */
func disconnect() func disconnect() async
/** /**
Uninstalls the VPN profile. 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
}
}
} }