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
- 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.

View File

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

View File

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

View File

@ -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() {

View File

@ -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() {

View File

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

View File

@ -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)
)
}

View File

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

View File

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

View File

@ -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;

View File

@ -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.

View File

@ -38,13 +38,21 @@ public class MockVPN: VPN {
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)
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)
}

View File

@ -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<Void, Never>) in
NETunnelProviderManager.loadAllFromPreferences { managers, error in
continuation.resume()
}
}
}
public func install(
_ tunnelBundleIdentifier: String,
configuration: NetworkExtensionConfiguration,
extra: NetworkExtensionExtra?,
completionHandler: ((Result<NETunnelProviderManager, Error>) -> 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<Void, Never>) 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<Void, Never>) 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<NETunnelProviderManager, Error>) -> 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<Void, Never>) 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) {

View File

@ -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<Manager, Error>) -> 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
}
}
}