diff --git a/.jazzy.yaml b/.jazzy.yaml index 54e006d..b8f6cef 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -43,16 +43,19 @@ custom_categories: - NETCPSocket - NETunnelInterface - NEUDPSocket - - NSNotification - name: Manager children: - VPN - - VPNProvider - - MockVPNProvider - VPNConfiguration - - NetworkExtensionVPNConfiguration + - VPNProvider + - VPNProviderIPC - VPNStatus - - NSNotification + - NetworkExtensionLocator + - NetworkExtensionNativeLocator + - NetworkExtensionTunnelLocator + - NetworkExtensionVPNConfiguration + - NetworkExtensionVPNProvider + - MockVPNProvider - name: Protocols/OpenVPN children: - OpenVPN diff --git a/README.md b/README.md index 70d2827..1025b34 100644 --- a/README.md +++ b/README.md @@ -164,8 +164,6 @@ This subspec includes convenient classes to control the VPN tunnel from your app - `MockVPNProvider` (default, useful to test on simulator) - `OpenVPNProvider` -Set `VPN.shared` to either of them at app launch time. - ### Protocols/OpenVPN Here you will find the low-level entities on top of which an OpenVPN connection is established. Code is mixed Swift and Obj-C, most of it is not exposed to consumers. The module depends on OpenSSL. diff --git a/TunnelKit.podspec b/TunnelKit.podspec index 272ce0f..8b051c1 100644 --- a/TunnelKit.podspec +++ b/TunnelKit.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "TunnelKit" - s.version = "3.4.0" - s.summary = "Non-official OpenVPN client for Apple platforms." + s.version = "3.5.0" + s.summary = "VPN client library for Apple platforms." s.homepage = "https://github.com/passepartoutvpn/tunnelkit" s.license = { :type => "GPLv3", :file => "LICENSE" } diff --git a/TunnelKit.xcodeproj/project.pbxproj b/TunnelKit.xcodeproj/project.pbxproj index 074673f..0dbee0a 100644 --- a/TunnelKit.xcodeproj/project.pbxproj +++ b/TunnelKit.xcodeproj/project.pbxproj @@ -190,6 +190,14 @@ 0E94E8EB25BACEBD0040BC30 /* DNSProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E94E8E925BACEBD0040BC30 /* DNSProtocol.swift */; }; 0EA82A282190B220007960EB /* TunnelKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E3251C51F95770D00C108D9 /* TunnelKit.framework */; }; 0EA82A3E2190B2BC007960EB /* pia-2048.pem in Resources */ = {isa = PBXBuildFile; fileRef = 0E749F612178911C00BB2701 /* pia-2048.pem */; }; + 0EA9F28926D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28826D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift */; }; + 0EA9F28A26D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28826D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift */; }; + 0EA9F28C26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28B26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift */; }; + 0EA9F28D26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28B26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift */; }; + 0EA9F28F26D6AC6D00E806E1 /* VPNProviderIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28E26D6AC6D00E806E1 /* VPNProviderIPC.swift */; }; + 0EA9F29026D6AC6D00E806E1 /* VPNProviderIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28E26D6AC6D00E806E1 /* VPNProviderIPC.swift */; }; + 0EA9F29526D6CA7E00E806E1 /* NetworkExtensionLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F29426D6CA7E00E806E1 /* NetworkExtensionLocator.swift */; }; + 0EA9F29626D6CA7E00E806E1 /* NetworkExtensionLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F29426D6CA7E00E806E1 /* NetworkExtensionLocator.swift */; }; 0EAC57372494277A00D0FCE0 /* OpenVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAC57312494277A00D0FCE0 /* OpenVPNProvider.swift */; }; 0EAC57382494277A00D0FCE0 /* OpenVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAC57312494277A00D0FCE0 /* OpenVPNProvider.swift */; }; 0EAC57392494277A00D0FCE0 /* MockVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAC57322494277A00D0FCE0 /* MockVPNProvider.swift */; }; @@ -571,6 +579,10 @@ 0E85A25B202CCA3D0059E9F9 /* Host.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Host.entitlements; sourceTree = ""; }; 0E94E8E925BACEBD0040BC30 /* DNSProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSProtocol.swift; sourceTree = ""; }; 0EA82A232190B220007960EB /* TunnelKitTests-macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "TunnelKitTests-macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 0EA9F28826D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkExtensionVPNConfiguration.swift; sourceTree = ""; }; + 0EA9F28B26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionVPNProvider.swift; sourceTree = ""; }; + 0EA9F28E26D6AC6D00E806E1 /* VPNProviderIPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNProviderIPC.swift; sourceTree = ""; }; + 0EA9F29426D6CA7E00E806E1 /* NetworkExtensionLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkExtensionLocator.swift; sourceTree = ""; }; 0EAC57312494277A00D0FCE0 /* OpenVPNProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenVPNProvider.swift; sourceTree = ""; }; 0EAC57322494277A00D0FCE0 /* MockVPNProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockVPNProvider.swift; sourceTree = ""; }; 0EAC57332494277A00D0FCE0 /* VPNStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNStatus.swift; sourceTree = ""; }; @@ -1033,9 +1045,13 @@ isa = PBXGroup; children = ( 0EAC57322494277A00D0FCE0 /* MockVPNProvider.swift */, + 0EA9F29426D6CA7E00E806E1 /* NetworkExtensionLocator.swift */, + 0EA9F28826D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift */, + 0EA9F28B26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift */, 0EAC57362494277A00D0FCE0 /* VPN.swift */, 0EAC57342494277A00D0FCE0 /* VPNConfiguration.swift */, 0EAC57352494277A00D0FCE0 /* VPNProvider.swift */, + 0EA9F28E26D6AC6D00E806E1 /* VPNProviderIPC.swift */, 0EAC57332494277A00D0FCE0 /* VPNStatus.swift */, ); path = Manager; @@ -1989,6 +2005,7 @@ 0E23B3DE229749C600304C30 /* LinkProducer.swift in Sources */, 0E23B42722982AF800304C30 /* CryptoAEAD.m in Sources */, 0EE2F9F222918DA100F56F49 /* NETunnelInterface.swift in Sources */, + 0EA9F28C26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift in Sources */, 0E23B46722982AF800304C30 /* Authenticator.swift in Sources */, 0E23B43922982AF800304C30 /* OpenVPNError.swift in Sources */, 0EAC573F2494277A00D0FCE0 /* VPNProvider.swift in Sources */, @@ -2027,6 +2044,7 @@ 0E23B43522982AF800304C30 /* CoreConfiguration+OpenVPN.swift in Sources */, 0EFEB46D2006D3C800F81029 /* Data+Manipulation.swift in Sources */, 0E23B46B22982AF800304C30 /* CryptoCBC.m in Sources */, + 0EA9F28926D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift in Sources */, 0E23B46522982AF800304C30 /* EncryptionBridge.swift in Sources */, 0EE2F9F422918DA100F56F49 /* NWTCPConnectionState+Description.swift in Sources */, 0EFEB4742006D3C800F81029 /* CoreConfiguration.swift in Sources */, @@ -2049,6 +2067,7 @@ 0E23B43722982AF800304C30 /* TLSBox.m in Sources */, 0EFEB4702006D3C800F81029 /* Allocation.m in Sources */, 0EE2FA0A22918DA100F56F49 /* InterfaceObserver.swift in Sources */, + 0EA9F28F26D6AC6D00E806E1 /* VPNProviderIPC.swift in Sources */, 0E23B43F22982AF800304C30 /* OpenVPNSession.swift in Sources */, 0E23B44722982AF800304C30 /* OpenVPN.swift in Sources */, 0ECC60D82254981A0020BEAC /* ConfigurationError.swift in Sources */, @@ -2061,6 +2080,7 @@ 0E23B45922982AF800304C30 /* OpenVPNTunnelProvider.swift in Sources */, 0EFEB45A2006D3C800F81029 /* TunnelInterface.swift in Sources */, 0EAC57372494277A00D0FCE0 /* OpenVPNProvider.swift in Sources */, + 0EA9F29526D6CA7E00E806E1 /* NetworkExtensionLocator.swift in Sources */, 0E23B47322982AF800304C30 /* MSS.m in Sources */, 0EAC573D2494277A00D0FCE0 /* VPNConfiguration.swift in Sources */, 0E23B45D22982AF800304C30 /* ProtocolMacros.swift in Sources */, @@ -2079,6 +2099,7 @@ 0E23B3DF229749C600304C30 /* LinkProducer.swift in Sources */, 0E23B42822982AF800304C30 /* CryptoAEAD.m in Sources */, 0EE2F9F322918DA100F56F49 /* NETunnelInterface.swift in Sources */, + 0EA9F28D26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift in Sources */, 0E23B46822982AF800304C30 /* Authenticator.swift in Sources */, 0E23B43A22982AF800304C30 /* OpenVPNError.swift in Sources */, 0EAC57402494277A00D0FCE0 /* VPNProvider.swift in Sources */, @@ -2117,6 +2138,7 @@ 0E23B43622982AF800304C30 /* CoreConfiguration+OpenVPN.swift in Sources */, 0EFB902C22788512006405E4 /* RoutingTable.m in Sources */, 0E23B46C22982AF800304C30 /* CryptoCBC.m in Sources */, + 0EA9F28A26D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift in Sources */, 0E23B46622982AF800304C30 /* EncryptionBridge.swift in Sources */, 0EFEB4952006D7F300F81029 /* SecureRandom.swift in Sources */, 0EE2F9F522918DA100F56F49 /* NWTCPConnectionState+Description.swift in Sources */, @@ -2139,6 +2161,7 @@ 0E23B43822982AF800304C30 /* TLSBox.m in Sources */, 0EFEB4902006D7F300F81029 /* TunnelInterface.swift in Sources */, 0EFEB49E2006D7F300F81029 /* Allocation.m in Sources */, + 0EA9F29026D6AC6D00E806E1 /* VPNProviderIPC.swift in Sources */, 0E23B44022982AF800304C30 /* OpenVPNSession.swift in Sources */, 0E23B44822982AF800304C30 /* OpenVPN.swift in Sources */, 0ECC60D92254981A0020BEAC /* ConfigurationError.swift in Sources */, @@ -2151,6 +2174,7 @@ 0E23B45A22982AF800304C30 /* OpenVPNTunnelProvider.swift in Sources */, 0E011F7B2196D93600BA59EE /* SocketType.swift in Sources */, 0EAC57382494277A00D0FCE0 /* OpenVPNProvider.swift in Sources */, + 0EA9F29626D6CA7E00E806E1 /* NetworkExtensionLocator.swift in Sources */, 0E23B47422982AF800304C30 /* MSS.m in Sources */, 0EAC573E2494277A00D0FCE0 /* VPNConfiguration.swift in Sources */, 0E23B45E22982AF800304C30 /* ProtocolMacros.swift in Sources */, diff --git a/TunnelKit/Sources/Manager/MockVPNProvider.swift b/TunnelKit/Sources/Manager/MockVPNProvider.swift index 4b7bb76..aa52c46 100644 --- a/TunnelKit/Sources/Manager/MockVPNProvider.swift +++ b/TunnelKit/Sources/Manager/MockVPNProvider.swift @@ -25,8 +25,15 @@ import Foundation -/// :nodoc: -public class MockVPNProvider: VPNProvider { +/// Simulates a VPN provider. +public class MockVPNProvider: VPNProvider, VPNProviderIPC { + + /// :nodoc: + public init() { + } + + // MARK: VPNProvider + public let isPrepared: Bool = true public private(set) var isEnabled: Bool = false @@ -71,6 +78,8 @@ public class MockVPNProvider: VPNProvider { completionHandler?() } + // MARK: VPNProviderIPC + public func requestDebugLog(fallback: (() -> String)?, completionHandler: @escaping (String) -> Void) { let log = [String](repeating: "lorem ipsum", count: 1000).joined(separator: " ") completionHandler(log) diff --git a/TunnelKit/Sources/Manager/NetworkExtensionLocator.swift b/TunnelKit/Sources/Manager/NetworkExtensionLocator.swift new file mode 100644 index 0000000..b866d09 --- /dev/null +++ b/TunnelKit/Sources/Manager/NetworkExtensionLocator.swift @@ -0,0 +1,91 @@ +// +// NetworkExtensionLocator.swift +// TunnelKit +// +// Created by Davide De Rosa on 8/25/21. +// Copyright (c) 2021 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of TunnelKit. +// +// TunnelKit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TunnelKit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TunnelKit. If not, see . +// + +import Foundation +import NetworkExtension + +/// Entity able to look up a `NEVPNManager`. +public protocol NetworkExtensionLocator { + + /** + Looks up the VPN manager. + + - Parameter completionHandler: The completion handler with a `NEVPNManager` or an error (if not found). + */ + func lookup(completionHandler: @escaping (NEVPNManager?, Error?) -> Void) +} + +/// Locator for native VPN protocols. +public class NetworkExtensionNativeLocator: NetworkExtensionLocator { + + /// :nodoc: + public init() { + } + + // MARK: NetworkExtensionLocator + + public func lookup(completionHandler: @escaping (NEVPNManager?, Error?) -> Void) { + let manager = NEVPNManager.shared() + manager.loadFromPreferences { (error) in + guard error == nil else { + completionHandler(nil, error) + return + } + completionHandler(manager, nil) + } + } +} + +/// Locator for tunnel VPN protocols. +public class NetworkExtensionTunnelLocator: NetworkExtensionLocator { + private let bundleIdentifier: String + + /** + Initializes the locator with the bundle identifier of the tunnel provider. + + - Parameter bundleIdentifier: The bundle identifier of the tunnel provider. + */ + public init(bundleIdentifier: String) { + self.bundleIdentifier = bundleIdentifier + } + + // MARK: NetworkExtensionLocator + + public func lookup(completionHandler: @escaping (NEVPNManager?, Error?) -> Void) { + NETunnelProviderManager.loadAllFromPreferences { (managers, error) in + guard error == nil else { + completionHandler(nil, error) + return + } + let manager = managers?.first { + guard let ptm = $0.protocolConfiguration as? NETunnelProviderProtocol else { + return false + } + return (ptm.providerBundleIdentifier == self.bundleIdentifier) + } + completionHandler(manager ?? NETunnelProviderManager(), nil) + } + } +} diff --git a/TunnelKit/Sources/Manager/NetworkExtensionVPNConfiguration.swift b/TunnelKit/Sources/Manager/NetworkExtensionVPNConfiguration.swift new file mode 100644 index 0000000..a75b809 --- /dev/null +++ b/TunnelKit/Sources/Manager/NetworkExtensionVPNConfiguration.swift @@ -0,0 +1,47 @@ +// +// NetworkExtensionVPNConfiguration.swift +// TunnelKit +// +// Created by Davide De Rosa on 8/25/21. +// Copyright (c) 2021 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of TunnelKit. +// +// TunnelKit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TunnelKit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TunnelKit. If not, see . +// + +import Foundation +import NetworkExtension + +/// A `VPNConfiguration` built on top of NetworkExtension entities. +public struct NetworkExtensionVPNConfiguration: VPNConfiguration { + + /// :nodoc: + public var title: String + + /// The `NEVPNProtocol` object embedding tunnel configuration. + public let protocolConfiguration: NEVPNProtocol + + /// The on-demand rules to establish. + public let onDemandRules: [NEOnDemandRule] + + /// :nodoc: + public init(title: String, protocolConfiguration: NEVPNProtocol, onDemandRules: [NEOnDemandRule]) { + self.title = title + self.protocolConfiguration = protocolConfiguration + self.onDemandRules = onDemandRules + } +} diff --git a/TunnelKit/Sources/Manager/NetworkExtensionVPNProvider.swift b/TunnelKit/Sources/Manager/NetworkExtensionVPNProvider.swift new file mode 100644 index 0000000..334fb3d --- /dev/null +++ b/TunnelKit/Sources/Manager/NetworkExtensionVPNProvider.swift @@ -0,0 +1,212 @@ +// +// NetworkExtensionVPNProvider.swift +// TunnelKit +// +// Created by Davide De Rosa on 6/15/18. +// Copyright (c) 2021 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of TunnelKit. +// +// TunnelKit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TunnelKit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TunnelKit. If not, see . +// + +import Foundation +import NetworkExtension +import SwiftyBeaver + +private let log = SwiftyBeaver.self + +/// `VPNProvider` based on the NetworkExtension framework. +public class NetworkExtensionVPNProvider: VPNProvider { + private var manager: NEVPNManager? + + private let locator: NetworkExtensionLocator + + private var lastNotifiedStatus: VPNStatus? + + /** + Initializes a provider with a `NetworkExtensionLocator`. + + - Parameter locator: A `NetworkExtensionLocator` able to locate a `NEVPNManager`. + */ + public init(locator: NetworkExtensionLocator) { + self.locator = locator + + let nc = NotificationCenter.default + nc.addObserver(self, selector: #selector(vpnDidUpdate(_:)), name: .NEVPNStatusDidChange, object: nil) + nc.addObserver(self, selector: #selector(vpnDidReinstall(_:)), name: .NEVPNConfigurationChange, object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: VPNProvider + + public var isPrepared: Bool { + return manager != nil + } + + public var isEnabled: Bool { + guard let manager = manager else { + return false + } + return manager.isEnabled && manager.isOnDemandEnabled + } + + public var status: VPNStatus { + guard let neStatus = manager?.connection.status else { + return .disconnected + } + switch neStatus { + case .connected: + return .connected + + case .connecting, .reasserting: + return .connecting + + case .disconnecting: + return .disconnecting + + case .disconnected, .invalid: + return .disconnected + + @unknown default: + return .disconnected + } + } + + public func prepare(completionHandler: (() -> Void)?) { + locator.lookup { manager, error in + self.manager = manager + NotificationCenter.default.post(name: VPN.didPrepare, object: nil) + completionHandler?() + } + } + + public func install(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) { + guard let configuration = configuration as? NetworkExtensionVPNConfiguration else { + fatalError("Not a NetworkExtensionVPNConfiguration") + } + locator.lookup { manager, error in + guard let manager = manager else { + completionHandler?(error) + return + } + self.manager = manager + manager.localizedDescription = configuration.title + manager.protocolConfiguration = configuration.protocolConfiguration + manager.onDemandRules = configuration.onDemandRules + manager.isOnDemandEnabled = true + manager.isEnabled = true + manager.saveToPreferences { error in + guard error == nil else { + manager.isOnDemandEnabled = false + manager.isEnabled = false + completionHandler?(error) + return + } + manager.loadFromPreferences { error in + completionHandler?(error) + } + } + } + } + + public func connect(completionHandler: ((Error?) -> Void)?) { + do { + try manager?.connection.startVPNTunnel() + completionHandler?(nil) + } catch let e { + completionHandler?(e) + } + } + + public func disconnect(completionHandler: ((Error?) -> Void)?) { + guard let manager = manager else { + completionHandler?(nil) + return + } + manager.connection.stopVPNTunnel() + manager.isOnDemandEnabled = false + manager.isEnabled = false + manager.saveToPreferences(completionHandler: completionHandler) + } + + public func reconnect(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) { + guard let configuration = configuration as? NetworkExtensionVPNConfiguration else { + fatalError("Not a NetworkExtensionVPNConfiguration") + } + install(configuration: configuration) { error in + guard error == nil else { + completionHandler?(error) + return + } + let connectBlock = { + self.connect(completionHandler: completionHandler) + } + if self.status != .disconnected { + self.manager?.connection.stopVPNTunnel() + DispatchQueue.main.asyncAfter(deadline: .now() + CoreConfiguration.reconnectionDelay, execute: connectBlock) + } else { + connectBlock() + } + } + } + + public func uninstall(completionHandler: (() -> Void)?) { + locator.lookup { manager, error in + guard let manager = manager else { + completionHandler?() + return + } + manager.connection.stopVPNTunnel() + manager.removeFromPreferences { error in + self.manager = nil + completionHandler?() + } + } + } + + // MARK: Helpers + + func lookup(completionHandler: @escaping (NEVPNManager?, Error?) -> Void) { + locator.lookup(completionHandler: completionHandler) + } + + // MARK: Notifications + + @objc private func vpnDidUpdate(_ notification: Notification) { + guard let connection = notification.object as? NETunnelProviderSession else { + return + } + log.debug("VPN status did change: \(connection.status.rawValue)") + + let status = self.status + if let last = lastNotifiedStatus { + guard status != last else { + return + } + } + lastNotifiedStatus = status + + NotificationCenter.default.post(name: VPN.didChangeStatus, object: self) + } + + @objc private func vpnDidReinstall(_ notification: Notification) { + NotificationCenter.default.post(name: VPN.didReinstall, object: self) + } +} diff --git a/TunnelKit/Sources/Manager/VPN.swift b/TunnelKit/Sources/Manager/VPN.swift index d3120ef..15ae8ab 100644 --- a/TunnelKit/Sources/Manager/VPN.swift +++ b/TunnelKit/Sources/Manager/VPN.swift @@ -36,7 +36,4 @@ public class VPN { /// The VPN profile did (re)install. public static let didReinstall = Notification.Name("VPNDidReinstall") - - /// A singleton `VPNProvider` instance (default is a `MockVPNProvider`). Make sure to set this on app launch. - public static var shared: VPNProvider = MockVPNProvider() } diff --git a/TunnelKit/Sources/Manager/VPNConfiguration.swift b/TunnelKit/Sources/Manager/VPNConfiguration.swift index 6d629dc..f23d9c3 100644 --- a/TunnelKit/Sources/Manager/VPNConfiguration.swift +++ b/TunnelKit/Sources/Manager/VPNConfiguration.swift @@ -24,7 +24,6 @@ // import Foundation -import NetworkExtension /// Generic marker for objects able to configure a `VPNProvider`. public protocol VPNConfiguration { @@ -32,23 +31,3 @@ public protocol VPNConfiguration { /// The profile title in device settings. var title: String { get } } - -/// A `VPNConfiguration` built on top of NetworkExtension entities. -public struct NetworkExtensionVPNConfiguration: VPNConfiguration { - - /// :nodoc: - public var title: String - - /// The `NETunnelProviderProtocol` object embedding tunnel configuration. - public let protocolConfiguration: NETunnelProviderProtocol - - /// The on-demand rules to establish. - public let onDemandRules: [NEOnDemandRule] - - /// :nodoc: - public init(title: String, protocolConfiguration: NETunnelProviderProtocol, onDemandRules: [NEOnDemandRule]) { - self.title = title - self.protocolConfiguration = protocolConfiguration - self.onDemandRules = onDemandRules - } -} diff --git a/TunnelKit/Sources/Manager/VPNProvider.swift b/TunnelKit/Sources/Manager/VPNProvider.swift index 565d721..3d534c2 100644 --- a/TunnelKit/Sources/Manager/VPNProvider.swift +++ b/TunnelKit/Sources/Manager/VPNProvider.swift @@ -82,26 +82,4 @@ public protocol VPNProvider: AnyObject { - Parameter completionHandler: The completion handler. */ func uninstall(completionHandler: (() -> Void)?) - - /** - Request a debug log from the VPN. - - - Parameter fallback: The block resolving to a fallback `String` if no debug log is available. - - Parameter completionHandler: The completion handler with the debug log. - */ - func requestDebugLog(fallback: (() -> String)?, completionHandler: @escaping (String) -> Void) - - /** - Requests the current received/sent bytes count from the VPN. - - - Parameter completionHandler: The completion handler with an optional received/sent bytes count. - */ - func requestBytesCount(completionHandler: @escaping ((UInt, UInt)?) -> Void) - - /** - Requests the server configuration from the VPN. - - - Parameter completionHandler: The completion handler with an optional configuration object. - */ - func requestServerConfiguration(completionHandler: @escaping (Any?) -> Void) } diff --git a/TunnelKit/Sources/Manager/VPNProviderIPC.swift b/TunnelKit/Sources/Manager/VPNProviderIPC.swift new file mode 100644 index 0000000..22b7110 --- /dev/null +++ b/TunnelKit/Sources/Manager/VPNProviderIPC.swift @@ -0,0 +1,52 @@ +// +// VPNProviderIPC.swift +// TunnelKit +// +// Created by Davide De Rosa on 8/25/21. +// Copyright (c) 2021 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of TunnelKit. +// +// TunnelKit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TunnelKit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TunnelKit. If not, see . +// + +import Foundation + +/// Common IPC functions supported by interactive VPN providers. +public protocol VPNProviderIPC { + + /** + Request a debug log from the VPN. + + - Parameter fallback: The block resolving to a fallback `String` if no debug log is available. + - Parameter completionHandler: The completion handler with the debug log. + */ + func requestDebugLog(fallback: (() -> String)?, completionHandler: @escaping (String) -> Void) + + /** + Requests the current received/sent bytes count from the VPN. + + - Parameter completionHandler: The completion handler with an optional received/sent bytes count. + */ + func requestBytesCount(completionHandler: @escaping ((UInt, UInt)?) -> Void) + + /** + Requests the server configuration from the VPN. + + - Parameter completionHandler: The completion handler with an optional configuration object. + */ + func requestServerConfiguration(completionHandler: @escaping (Any?) -> Void) +} diff --git a/TunnelKit/Sources/Protocols/Native/NativeProvider.swift b/TunnelKit/Sources/Protocols/Native/NativeProvider.swift new file mode 100644 index 0000000..9b7bde1 --- /dev/null +++ b/TunnelKit/Sources/Protocols/Native/NativeProvider.swift @@ -0,0 +1,74 @@ +// +// NativeProvider.swift +// TunnelKit +// +// Created by Davide De Rosa on 4/11/21. +// Copyright (c) 2021 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of TunnelKit. +// +// TunnelKit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TunnelKit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TunnelKit. If not, see . +// + +import Foundation + +/// `VPNProvider` for native IPSec/IKEv2 configurations. +public class NativeProvider: VPNProvider { + private let provider: NetworkExtensionVPNProvider + + /// :nodoc: + public init() { + provider = NetworkExtensionVPNProvider(locator: NetworkExtensionNativeLocator()) + } + + // MARK: VPNProvider + + public var isPrepared: Bool { + return provider.isPrepared + } + + public var isEnabled: Bool { + return provider.isEnabled + } + + public var status: VPNStatus { + return provider.status + } + + public func prepare(completionHandler: (() -> Void)?) { + provider.prepare(completionHandler: completionHandler) + } + + public func install(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) { + provider.install(configuration: configuration, completionHandler: completionHandler) + } + + public func connect(completionHandler: ((Error?) -> Void)?) { + provider.connect(completionHandler: completionHandler) + } + + public func disconnect(completionHandler: ((Error?) -> Void)?) { + provider.disconnect(completionHandler: completionHandler) + } + + public func reconnect(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) { + provider.reconnect(configuration: configuration, completionHandler: completionHandler) + } + + public func uninstall(completionHandler: (() -> Void)?) { + provider.uninstall(completionHandler: completionHandler) + } +} diff --git a/TunnelKit/Sources/Protocols/OpenVPN/OpenVPNProvider.swift b/TunnelKit/Sources/Protocols/OpenVPN/OpenVPNProvider.swift index 0d9d85c..d9a0a1f 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/OpenVPNProvider.swift +++ b/TunnelKit/Sources/Protocols/OpenVPN/OpenVPNProvider.swift @@ -26,155 +26,61 @@ import Foundation import NetworkExtension -/// :nodoc: -public class OpenVPNProvider: VPNProvider { - private let bundleIdentifier: String - - private var manager: NETunnelProviderManager? - - private var lastNotifiedStatus: VPNStatus? +/// `VPNProvider` for OpenVPN protocol. +public class OpenVPNProvider: VPNProvider, VPNProviderIPC { + private let provider: NetworkExtensionVPNProvider + /** + Initializes a provider with the bundle identifier of the `OpenVPNTunnelProvider`. + + - Parameter bundleIdentifier: The bundle identifier of the `OpenVPNTunnelProvider`. + */ public init(bundleIdentifier: String) { - self.bundleIdentifier = bundleIdentifier - - let nc = NotificationCenter.default - nc.addObserver(self, selector: #selector(vpnDidUpdate(_:)), name: .NEVPNStatusDidChange, object: nil) - nc.addObserver(self, selector: #selector(vpnDidReinstall(_:)), name: .NEVPNConfigurationChange, object: nil) + provider = NetworkExtensionVPNProvider(locator: NetworkExtensionTunnelLocator(bundleIdentifier: bundleIdentifier)) } - - deinit { - NotificationCenter.default.removeObserver(self) - } - + // MARK: VPNProvider public var isPrepared: Bool { - return manager != nil + return provider.isPrepared } public var isEnabled: Bool { - guard let manager = manager else { - return false - } - return manager.isEnabled && manager.isOnDemandEnabled + return provider.isEnabled } public var status: VPNStatus { - guard let neStatus = manager?.connection.status else { - return .disconnected - } - switch neStatus { - case .connected: - return .connected - - case .connecting, .reasserting: - return .connecting - - case .disconnecting: - return .disconnecting - - case .disconnected, .invalid: - return .disconnected - - @unknown default: - return .disconnected - } + return provider.status } public func prepare(completionHandler: (() -> Void)?) { - find(with: bundleIdentifier) { - self.manager = $0 - NotificationCenter.default.post(name: VPN.didPrepare, object: nil) - completionHandler?() - } + provider.prepare(completionHandler: completionHandler) } public func install(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) { - guard let configuration = configuration as? NetworkExtensionVPNConfiguration else { - fatalError("Not a NetworkExtensionVPNConfiguration") - } - find(with: bundleIdentifier) { - guard let manager = $0 else { - completionHandler?(nil) - return - } - self.manager = manager - manager.localizedDescription = configuration.title - manager.protocolConfiguration = configuration.protocolConfiguration - manager.onDemandRules = configuration.onDemandRules - manager.isOnDemandEnabled = true - manager.isEnabled = true - manager.saveToPreferences { (error) in - guard error == nil else { - manager.isOnDemandEnabled = false - manager.isEnabled = false - completionHandler?(error) - return - } - manager.loadFromPreferences { (error) in - completionHandler?(error) - } - } - } + provider.install(configuration: configuration, completionHandler: completionHandler) } public func connect(completionHandler: ((Error?) -> Void)?) { - do { - try manager?.connection.startVPNTunnel() - completionHandler?(nil) - } catch let e { - completionHandler?(e) - } + provider.connect(completionHandler: completionHandler) } public func disconnect(completionHandler: ((Error?) -> Void)?) { - guard let manager = manager else { - completionHandler?(nil) - return - } - manager.connection.stopVPNTunnel() - manager.isOnDemandEnabled = false - manager.isEnabled = false - manager.saveToPreferences(completionHandler: completionHandler) + provider.disconnect(completionHandler: completionHandler) } public func reconnect(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) { - guard let configuration = configuration as? NetworkExtensionVPNConfiguration else { - fatalError("Not a NetworkExtensionVPNConfiguration") - } - install(configuration: configuration) { (error) in - guard error == nil else { - completionHandler?(error) - return - } - let connectBlock = { - self.connect(completionHandler: completionHandler) - } - if self.status != .disconnected { - self.manager?.connection.stopVPNTunnel() - DispatchQueue.main.asyncAfter(deadline: .now() + CoreConfiguration.reconnectionDelay, execute: connectBlock) - } else { - connectBlock() - } - } + provider.reconnect(configuration: configuration, completionHandler: completionHandler) } public func uninstall(completionHandler: (() -> Void)?) { - find(with: bundleIdentifier) { (manager) in - guard let manager = manager else { - completionHandler?() - return - } - manager.connection.stopVPNTunnel() - manager.removeFromPreferences { (error) in - self.manager = nil - completionHandler?() - } - } + provider.uninstall(completionHandler: completionHandler) } + // MARK: VPNProviderIPC + public func requestDebugLog(fallback: (() -> String)?, completionHandler: @escaping (String) -> Void) { - guard status != .disconnected else { + guard provider.status != .disconnected else { completionHandler(fallback?() ?? "") return } @@ -188,11 +94,10 @@ public class OpenVPNProvider: VPNProvider { } } } - + public func requestBytesCount(completionHandler: @escaping ((UInt, UInt)?) -> Void) { - find(with: bundleIdentifier) { - self.manager = $0 - guard let session = self.manager?.connection as? NETunnelProviderSession else { + provider.lookup { manager, error in + guard let session = manager?.connection as? NETunnelProviderSession else { DispatchQueue.main.async { completionHandler(nil) } @@ -219,11 +124,10 @@ public class OpenVPNProvider: VPNProvider { } } } - + public func requestServerConfiguration(completionHandler: @escaping (Any?) -> Void) { - find(with: bundleIdentifier) { - self.manager = $0 - guard let session = self.manager?.connection as? NETunnelProviderSession else { + provider.lookup { manager, error in + guard let session = manager?.connection as? NETunnelProviderSession else { DispatchQueue.main.async { completionHandler(nil) } @@ -248,36 +152,19 @@ public class OpenVPNProvider: VPNProvider { } } } - + // MARK: Helpers - - private func find(with bundleIdentifier: String, completionHandler: @escaping (NETunnelProviderManager?) -> Void) { - NETunnelProviderManager.loadAllFromPreferences { (managers, error) in - guard error == nil else { - completionHandler(nil) - return - } - let manager = managers?.first { - guard let ptm = $0.protocolConfiguration as? NETunnelProviderProtocol else { - return false - } - return (ptm.providerBundleIdentifier == bundleIdentifier) - } - completionHandler(manager ?? NETunnelProviderManager()) - } - } private func findAndRequestDebugLog(completionHandler: @escaping (String?) -> Void) { - find(with: bundleIdentifier) { - self.manager = $0 - guard let session = self.manager?.connection as? NETunnelProviderSession else { + provider.lookup { manager, error in + guard let session = manager?.connection as? NETunnelProviderSession else { completionHandler(nil) return } OpenVPNProvider.requestDebugLog(session: session, completionHandler: completionHandler) } } - + private static func requestDebugLog(session: NETunnelProviderSession, completionHandler: @escaping (String?) -> Void) { do { try session.sendProviderMessage(OpenVPNTunnelProvider.Message.requestLog.data) { (data) in @@ -292,27 +179,4 @@ public class OpenVPNProvider: VPNProvider { completionHandler(nil) } } - - // MARK: Notifications - - @objc private func vpnDidUpdate(_ notification: Notification) { -// guard let connection = notification.object as? NETunnelProviderSession else { -// return -// } -// log.debug("VPN status did change: \(connection.status.rawValue)") - - let status = self.status - if let last = lastNotifiedStatus { - guard status != last else { - return - } - } - lastNotifiedStatus = status - - NotificationCenter.default.post(name: VPN.didChangeStatus, object: self) - } - - @objc private func vpnDidReinstall(_ notification: Notification) { - NotificationCenter.default.post(name: VPN.didReinstall, object: self) - } }