Make VPN public methods async
- With Swift Concurrency - Raise targets to iOS 13 / macOS 10.15
This commit is contained in:
parent
990a0b85a6
commit
e12e0b3051
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -109,7 +109,7 @@ class WireGuardViewController: NSViewController {
|
|||
tunnelIdentifier,
|
||||
configuration: cfg,
|
||||
extra: nil,
|
||||
delay: nil
|
||||
after: .seconds(2)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue