diff --git a/CHANGELOG.md b/CHANGELOG.md index e14e529..f5923c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- WireGuard: Support multiple peers. +- Manager package completely rewritten. +- Only enable on-demand if at least one rule is provided. +- Dropped incomplete support for IPSec/IKEv2. ## 4.1.0 (2022-02-09) diff --git a/Demo/Demo/Configuration.swift b/Demo/Demo/Configuration.swift index ad1b858..796fe9d 100644 --- a/Demo/Demo/Configuration.swift +++ b/Demo/Demo/Configuration.swift @@ -171,7 +171,7 @@ M69t86apMrAxkUxVJAWLRBd9fbYyzJgTW61tFqXWTZpiz6bhuWApSEzaHcL3/f5l -----END PRIVATE KEY----- """) - static func make(hostname: String, port: UInt16, socketType: SocketType) -> OpenVPNProvider.Configuration { + static func make(_ title: String, appGroup: String, hostname: String, port: UInt16, socketType: SocketType) -> OpenVPN.ProviderConfiguration { var sessionBuilder = OpenVPN.ConfigurationBuilder() sessionBuilder.ca = ca sessionBuilder.cipher = .aes128cbc @@ -182,10 +182,10 @@ M69t86apMrAxkUxVJAWLRBd9fbYyzJgTW61tFqXWTZpiz6bhuWApSEzaHcL3/f5l sessionBuilder.clientCertificate = clientCertificate sessionBuilder.clientKey = clientKey sessionBuilder.mtu = 1350 - var builder = OpenVPNProvider.ConfigurationBuilder(sessionConfiguration: sessionBuilder.build()) - builder.shouldDebug = true - builder.masksPrivateData = false - return builder.build() + var providerConfiguration = OpenVPN.ProviderConfiguration(title, appGroup: appGroup, configuration: sessionBuilder.build()) + providerConfiguration.shouldDebug = true + providerConfiguration.masksPrivateData = false + return providerConfiguration } } } @@ -193,12 +193,14 @@ M69t86apMrAxkUxVJAWLRBd9fbYyzJgTW61tFqXWTZpiz6bhuWApSEzaHcL3/f5l extension WireGuard { struct DemoConfiguration { static func make( + _ title: String, + appGroup: String, clientPrivateKey: String, clientAddress: String, serverPublicKey: String, serverAddress: String, serverPort: String - ) -> WireGuardProvider.Configuration? { + ) -> WireGuard.ProviderConfiguration? { var builder = WireGuard.ConfigurationBuilder(privateKey: clientPrivateKey) builder.addresses = [clientAddress] @@ -211,7 +213,7 @@ extension WireGuard { guard let cfg = builder.build() else { return nil } - return WireGuardProvider.Configuration(innerConfiguration: cfg) + return WireGuard.ProviderConfiguration(title, appGroup: appGroup, configuration: cfg) } } } diff --git a/Demo/Demo/iOS/OpenVPNViewController.swift b/Demo/Demo/iOS/OpenVPNViewController.swift index c6ba546..5124616 100644 --- a/Demo/Demo/iOS/OpenVPNViewController.swift +++ b/Demo/Demo/iOS/OpenVPNViewController.swift @@ -49,10 +49,14 @@ class OpenVPNViewController: UIViewController { @IBOutlet var textLog: UITextView! - private let vpn = OpenVPNProvider(bundleIdentifier: tunnelIdentifier) + private let vpn = NetworkExtensionVPN() + private var vpnStatus: VPNStatus = .disconnected + private let keychain = Keychain(group: appGroup) + private var cfg: OpenVPN.ProviderConfiguration? + override func viewDidLoad() { super.viewDidLoad() @@ -66,17 +70,23 @@ class OpenVPNViewController: UIViewController { NotificationCenter.default.addObserver( self, selector: #selector(VPNStatusDidChange(notification:)), - name: VPN.didChangeStatus, + name: VPNNotification.didChangeStatus, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(VPNDidFail(notification:)), + name: VPNNotification.didFail, object: nil ) - - vpn.prepare(completionHandler: nil) - testFetchRef() + vpn.prepare() + +// testFetchRef() } @IBAction func connectionClicked(_ sender: Any) { - switch vpn.status { + switch vpnStatus { case .disconnected: connect() @@ -96,34 +106,47 @@ class OpenVPNViewController: UIViewController { let socketType: SocketType = switchTCP.isOn ? .tcp : .udp let credentials = OpenVPN.Credentials(textUsername.text!, textPassword.text!) - let cfg = OpenVPN.DemoConfiguration.make(hostname: hostname, port: port, socketType: socketType) - let proto = try! cfg.generatedTunnelProtocol( - withBundleIdentifier: tunnelIdentifier, + cfg = OpenVPN.DemoConfiguration.make( + "TunnelKit.OpenVPN", appGroup: appGroup, - context: tunnelIdentifier, - credentials: credentials + hostname: hostname, + port: port, + socketType: socketType ) - let neCfg = NetworkExtensionVPNConfiguration(title: "TunnelKit.OpenVPN", protocolConfiguration: proto, onDemandRules: []) - vpn.reconnect(configuration: neCfg) { (error) in - if let error = error { - print("configure error: \(error)") - return - } + cfg?.username = credentials.username + + let passwordReference: Data + do { + passwordReference = try keychain.set(password: credentials.password, for: credentials.username, context: tunnelIdentifier) + } catch { + print("Keychain failure: \(error)") + return } + + var extra = NetworkExtensionExtra() + extra.passwordReference = passwordReference + + vpn.reconnect( + tunnelIdentifier, + configuration: cfg!, + extra: extra, + delay: nil + ) } func disconnect() { - vpn.disconnect(completionHandler: nil) + vpn.disconnect() } @IBAction func displayLog() { - vpn.requestDebugLog(fallback: { "" }) { (log) in - self.textLog.text = log + guard let cfg = cfg else { + return } + textLog.text = cfg.debugLog } func updateButton() { - switch vpn.status { + switch vpnStatus { case .connected, .connecting: buttonConnection.setTitle("Disconnect", for: .normal) @@ -135,26 +158,31 @@ class OpenVPNViewController: UIViewController { } } - @objc private func VPNStatusDidChange(notification: NSNotification) { - print("VPNStatusDidChange: \(vpn.status)") + @objc private func VPNStatusDidChange(notification: Notification) { + vpnStatus = notification.vpnStatus + print("VPNStatusDidChange: \(vpnStatus)") updateButton() } - - private func testFetchRef() { - let keychain = Keychain(group: appGroup) - let username = "foo" - let password = "bar" - - guard let ref = try? keychain.set(password: password, for: username, context: tunnelIdentifier) else { - print("Couldn't set password") - return - } - guard let fetchedPassword = try? Keychain.password(forReference: ref) else { - print("Couldn't fetch password") - return - } - print("\(username) -> \(password)") - print("\(username) -> \(fetchedPassword)") + @objc private func VPNDidFail(notification: Notification) { + print("VPNStatusDidFail: \(notification.vpnError.localizedDescription)") } + +// private func testFetchRef() { +// let keychain = Keychain(group: appGroup) +// let username = "foo" +// let password = "bar" +// +// guard let ref = try? keychain.set(password: password, for: username, context: tunnelIdentifier) else { +// print("Couldn't set password") +// return +// } +// guard let fetchedPassword = try? Keychain.password(forReference: ref) else { +// print("Couldn't fetch password") +// return +// } +// +// print("\(username) -> \(password)") +// print("\(username) -> \(fetchedPassword)") +// } } diff --git a/Demo/Demo/iOS/WireGuardViewController.swift b/Demo/Demo/iOS/WireGuardViewController.swift index 8a7df9b..95a7e68 100644 --- a/Demo/Demo/iOS/WireGuardViewController.swift +++ b/Demo/Demo/iOS/WireGuardViewController.swift @@ -26,7 +26,6 @@ import UIKit import TunnelKitManager import TunnelKitWireGuard -import NetworkExtension private let appGroup = "group.com.algoritmico.TunnelKit.Demo" @@ -45,7 +44,9 @@ class WireGuardViewController: UIViewController { @IBOutlet var buttonConnection: UIButton! - private let vpn = WireGuardProvider(bundleIdentifier: tunnelIdentifier) + private let vpn = NetworkExtensionVPN() + + private var vpnStatus: VPNStatus = .disconnected override func viewDidLoad() { super.viewDidLoad() @@ -61,15 +62,21 @@ class WireGuardViewController: UIViewController { NotificationCenter.default.addObserver( self, selector: #selector(VPNStatusDidChange(notification:)), - name: VPN.didChangeStatus, + name: VPNNotification.didChangeStatus, object: nil ) - - vpn.prepare(completionHandler: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(VPNDidFail(notification:)), + name: VPNNotification.didFail, + object: nil + ) + + vpn.prepare() } @IBAction func connectionClicked(_ sender: Any) { - switch vpn.status { + switch vpnStatus { case .disconnected: connect() @@ -86,6 +93,8 @@ class WireGuardViewController: UIViewController { let serverPort = textServerPort.text! guard let cfg = WireGuard.DemoConfiguration.make( + "TunnelKit.WireGuard", + appGroup: appGroup, clientPrivateKey: clientPrivateKey, clientAddress: clientAddress, serverPublicKey: serverPublicKey, @@ -95,27 +104,21 @@ class WireGuardViewController: UIViewController { print("Configuration incomplete") return } - let proto = try! cfg.generatedTunnelProtocol( - withBundleIdentifier: tunnelIdentifier, - appGroup: appGroup, - context: tunnelIdentifier - ) - let neCfg = NetworkExtensionVPNConfiguration(title: "TunnelKit.WireGuard", protocolConfiguration: proto, onDemandRules: []) - vpn.reconnect(configuration: neCfg) { (error) in - if let error = error { - print("configure error: \(error)") - return - } - } + vpn.reconnect( + tunnelIdentifier, + configuration: cfg, + extra: nil, + delay: nil + ) } func disconnect() { - vpn.disconnect(completionHandler: nil) + vpn.disconnect() } func updateButton() { - switch vpn.status { + switch vpnStatus { case .connected, .connecting: buttonConnection.setTitle("Disconnect", for: .normal) @@ -127,8 +130,12 @@ class WireGuardViewController: UIViewController { } } - @objc private func VPNStatusDidChange(notification: NSNotification) { - print("VPNStatusDidChange: \(vpn.status)") + @objc private func VPNStatusDidChange(notification: Notification) { + print("VPNStatusDidChange: \(notification.vpnStatus)") updateButton() } + + @objc private func VPNDidFail(notification: Notification) { + print("VPNStatusDidFail: \(notification.vpnError.localizedDescription)") + } } diff --git a/Demo/Demo/macOS/OpenVPNViewController.swift b/Demo/Demo/macOS/OpenVPNViewController.swift index 7ed96bc..fffadcb 100644 --- a/Demo/Demo/macOS/OpenVPNViewController.swift +++ b/Demo/Demo/macOS/OpenVPNViewController.swift @@ -45,10 +45,14 @@ class OpenVPNViewController: NSViewController { @IBOutlet var buttonConnection: NSButton! - private let vpn = OpenVPNProvider(bundleIdentifier: tunnelIdentifier) + private let vpn = NetworkExtensionVPN() + + private var vpnStatus: VPNStatus = .disconnected private let keychain = Keychain(group: appGroup) + private var cfg: OpenVPN.ProviderConfiguration? + override func viewDidLoad() { super.viewDidLoad() @@ -61,17 +65,23 @@ class OpenVPNViewController: NSViewController { NotificationCenter.default.addObserver( self, selector: #selector(VPNStatusDidChange(notification:)), - name: VPN.didChangeStatus, + name: VPNNotification.didChangeStatus, object: nil ) + NotificationCenter.default.addObserver( + self, + selector: #selector(VPNDidFail(notification:)), + name: VPNNotification.didFail, + object: nil + ) + + vpn.prepare() - vpn.prepare(completionHandler: nil) - - testFetchRef() +// testFetchRef() } @IBAction func connectionClicked(_ sender: Any) { - switch vpn.status { + switch vpnStatus { case .disconnected: connect() @@ -87,28 +97,40 @@ class OpenVPNViewController: NSViewController { let port = UInt16(textPort.stringValue)! let credentials = OpenVPN.Credentials(textUsername.stringValue, textPassword.stringValue) - let cfg = OpenVPN.DemoConfiguration.make(hostname: hostname, port: port, socketType: .udp) - let proto = try! cfg.generatedTunnelProtocol( - withBundleIdentifier: tunnelIdentifier, + cfg = OpenVPN.DemoConfiguration.make( + "TunnelKit.OpenVPN", appGroup: appGroup, - context: tunnelIdentifier, - credentials: credentials + hostname: hostname, + port: port, + socketType: .udp ) - let neCfg = NetworkExtensionVPNConfiguration(title: "TunnelKit.OpenVPN", protocolConfiguration: proto, onDemandRules: []) - vpn.reconnect(configuration: neCfg) { (error) in - if let error = error { - print("configure error: \(error)") - return - } + cfg?.username = credentials.username + + let passwordReference: Data + do { + passwordReference = try keychain.set(password: credentials.password, for: credentials.username, context: tunnelIdentifier) + } catch { + print("Keychain failure: \(error)") + return } + + var extra = NetworkExtensionExtra() + extra.passwordReference = passwordReference + + vpn.reconnect( + tunnelIdentifier, + configuration: cfg!, + extra: extra, + delay: nil + ) } func disconnect() { - vpn.disconnect(completionHandler: nil) + vpn.disconnect() } func updateButton() { - switch vpn.status { + switch vpnStatus { case .connected, .connecting: buttonConnection.title = "Disconnect" @@ -120,27 +142,32 @@ class OpenVPNViewController: NSViewController { } } - @objc private func VPNStatusDidChange(notification: NSNotification) { - print("VPNStatusDidChange: \(vpn.status)") + @objc private func VPNStatusDidChange(notification: Notification) { + vpnStatus = notification.vpnStatus + print("VPNStatusDidChange: \(vpnStatus)") updateButton() } - private func testFetchRef() { - let keychain = Keychain(group: appGroup) - let username = "foo" - let password = "bar" - - guard let ref = try? keychain.set(password: password, for: username, context: tunnelIdentifier) else { - print("Couldn't set password") - return - } - guard let fetchedPassword = try? Keychain.password(forReference: ref) else { - print("Couldn't fetch password") - return - } - - print("\(username) -> \(password)") - print("\(username) -> \(fetchedPassword)") + @objc private func VPNDidFail(notification: Notification) { + print("VPNStatusDidFail: \(notification.vpnError.localizedDescription)") } + +// private func testFetchRef() { +// let keychain = Keychain(group: appGroup) +// let username = "foo" +// let password = "bar" +// +// guard let ref = try? keychain.set(password: password, for: username, context: tunnelIdentifier) else { +// print("Couldn't set password") +// return +// } +// guard let fetchedPassword = try? Keychain.password(forReference: ref) else { +// print("Couldn't fetch password") +// return +// } +// +// print("\(username) -> \(password)") +// print("\(username) -> \(fetchedPassword)") +// } } diff --git a/Demo/Demo/macOS/WireGuardViewController.swift b/Demo/Demo/macOS/WireGuardViewController.swift index 610f40d..531c790 100644 --- a/Demo/Demo/macOS/WireGuardViewController.swift +++ b/Demo/Demo/macOS/WireGuardViewController.swift @@ -26,7 +26,6 @@ import Cocoa import TunnelKitManager import TunnelKitWireGuard -import NetworkExtension private let appGroup = "DTDYD63ZX9.group.com.algoritmico.TunnelKit.Demo" @@ -45,7 +44,9 @@ class WireGuardViewController: NSViewController { @IBOutlet var buttonConnection: NSButton! - private let vpn = WireGuardProvider(bundleIdentifier: tunnelIdentifier) + private let vpn = NetworkExtensionVPN() + + private var vpnStatus: VPNStatus = .disconnected override func viewDidLoad() { super.viewDidLoad() @@ -61,15 +62,21 @@ class WireGuardViewController: NSViewController { NotificationCenter.default.addObserver( self, selector: #selector(VPNStatusDidChange(notification:)), - name: VPN.didChangeStatus, + name: VPNNotification.didChangeStatus, object: nil ) - - vpn.prepare(completionHandler: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(VPNDidFail(notification:)), + name: VPNNotification.didFail, + object: nil + ) + + vpn.prepare() } @IBAction func connectionClicked(_ sender: Any) { - switch vpn.status { + switch vpnStatus { case .disconnected: connect() @@ -86,6 +93,8 @@ class WireGuardViewController: NSViewController { let serverPort = textServerPort.stringValue guard let cfg = WireGuard.DemoConfiguration.make( + "TunnelKit.WireGuard", + appGroup: appGroup, clientPrivateKey: clientPrivateKey, clientAddress: clientAddress, serverPublicKey: serverPublicKey, @@ -95,27 +104,21 @@ class WireGuardViewController: NSViewController { print("Configuration incomplete") return } - let proto = try! cfg.generatedTunnelProtocol( - withBundleIdentifier: tunnelIdentifier, - appGroup: appGroup, - context: tunnelIdentifier - ) - let neCfg = NetworkExtensionVPNConfiguration(title: "TunnelKit.WireGuard", protocolConfiguration: proto, onDemandRules: []) - vpn.reconnect(configuration: neCfg) { (error) in - if let error = error { - print("configure error: \(error)") - return - } - } + vpn.reconnect( + tunnelIdentifier, + configuration: cfg, + extra: nil, + delay: nil + ) } func disconnect() { - vpn.disconnect(completionHandler: nil) + vpn.disconnect() } func updateButton() { - switch vpn.status { + switch vpnStatus { case .connected, .connecting: buttonConnection.title = "Disconnect" @@ -127,8 +130,13 @@ class WireGuardViewController: NSViewController { } } - @objc private func VPNStatusDidChange(notification: NSNotification) { - print("VPNStatusDidChange: \(vpn.status)") + @objc private func VPNStatusDidChange(notification: Notification) { + vpnStatus = notification.vpnStatus + print("VPNStatusDidChange: \(vpnStatus)") updateButton() } + + @objc private func VPNDidFail(notification: Notification) { + print("VPNStatusDidFail: \(notification.vpnError.localizedDescription)") + } } diff --git a/Package.swift b/Package.swift index ecdaeda..b4402c3 100644 --- a/Package.swift +++ b/Package.swift @@ -14,10 +14,6 @@ let package = Package( name: "TunnelKit", targets: ["TunnelKit"] ), - .library( - name: "TunnelKitIKE", - targets: ["TunnelKitIKE"] - ), .library( name: "TunnelKitOpenVPN", targets: ["TunnelKitOpenVPN"] @@ -73,11 +69,6 @@ let package = Package( dependencies: [ "TunnelKitCore" ]), - .target( - name: "TunnelKitIKE", - dependencies: [ - "TunnelKitManager" - ]), .target( name: "TunnelKitOpenVPN", dependencies: [ @@ -122,6 +113,7 @@ let package = Package( .target( name: "TunnelKitWireGuardCore", dependencies: [ + "__TunnelKitUtils", "WireGuardKit" ]), .target( diff --git a/README.md b/README.md index 31feedb..ab2e4f8 100644 --- a/README.md +++ b/README.md @@ -164,14 +164,10 @@ Full documentation of the public interface is available and can be generated by ### TunnelKit -This component includes convenient classes to control the VPN tunnel from your app without the NetworkExtension headaches. Have a look at `VPNProvider` implementations: +This component includes convenient classes to control the VPN tunnel from your app without the NetworkExtension headaches. Have a look at `VPN` implementations: -- `MockVPNProvider` (default, useful to test on simulator) -- `NetworkExtensionVPNProvider` (anything based on NetworkExtension) - -### TunnelKitIKE - -Here you find `NativeProvider`, a generic way to manage a VPN profile based on the native IPSec/IKEv2 protocols. Just wrap a `NEVPNProtocolIPSec` or `NEVPNProtocolIKEv2` object in a `NetworkExtensionVPNConfiguration` and use it to install or connect to the VPN. +- `MockVPN` (default, useful to test on simulator) +- `NetworkExtensionVPN` (anything based on NetworkExtension) ### TunnelKitOpenVPN diff --git a/Sources/TunnelKitManager/VPNConfiguration.swift b/Sources/TunnelKitCore/DataCount.swift similarity index 65% rename from Sources/TunnelKitManager/VPNConfiguration.swift rename to Sources/TunnelKitCore/DataCount.swift index a7524a5..7e36f66 100644 --- a/Sources/TunnelKitManager/VPNConfiguration.swift +++ b/Sources/TunnelKitCore/DataCount.swift @@ -1,8 +1,8 @@ // -// VPNConfiguration.swift +// DataCount.swift // TunnelKit // -// Created by Davide De Rosa on 9/18/18. +// Created by Davide De Rosa on 3/5/22. // Copyright (c) 2022 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn @@ -25,9 +25,20 @@ import Foundation -/// Generic marker for objects able to configure a `VPNProvider`. -public protocol VPNConfiguration { +/// :nodoc: +public struct DataCount: Equatable { + public let received: UInt + + public let sent: UInt + + public init(_ received: UInt, _ sent: UInt) { + self.received = received + self.sent = sent + } - /// The profile title in device settings. - var title: String { get } + // MARK: Equatable + + public static func ==(lhs: DataCount, rhs: DataCount) -> Bool { + return lhs.up == rhs.up && lhs.down == rhs.down + } } diff --git a/Sources/TunnelKitCore/Session.swift b/Sources/TunnelKitCore/Session.swift index 29c706a..8e18386 100644 --- a/Sources/TunnelKitCore/Session.swift +++ b/Sources/TunnelKitCore/Session.swift @@ -66,9 +66,9 @@ public protocol Session { /** Returns the current data bytes count. - - Returns: The current data bytes count as a pair, inbound first. + - Returns: The current data bytes count. */ - func dataCount() -> (Int, Int)? + func dataCount() -> DataCount? /** Returns the current server configuration. diff --git a/Sources/TunnelKitIKE/NativeProvider.swift b/Sources/TunnelKitIKE/NativeProvider.swift deleted file mode 100644 index 4d77068..0000000 --- a/Sources/TunnelKitIKE/NativeProvider.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// NativeProvider.swift -// TunnelKit -// -// Created by Davide De Rosa on 4/11/21. -// Copyright (c) 2022 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 TunnelKitManager - -/// `VPNProvider` for native IPSec/IKEv2 configurations. -public class NativeProvider: VPNProvider { - private let provider: NetworkExtensionVPNProvider - - 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, delay: Double? = nil, completionHandler: ((Error?) -> Void)?) { - provider.reconnect(configuration: configuration, delay: delay, completionHandler: completionHandler) - } - - public func uninstall(completionHandler: (() -> Void)?) { - provider.uninstall(completionHandler: completionHandler) - } -} diff --git a/Sources/TunnelKitManager/Keychain.swift b/Sources/TunnelKitManager/Keychain.swift index e019ed9..84fcd8f 100644 --- a/Sources/TunnelKitManager/Keychain.swift +++ b/Sources/TunnelKitManager/Keychain.swift @@ -36,6 +36,11 @@ import Foundation +// Label -> Name +// Description -> Kind +// Service -> Where +// Account -> Account + /// Error raised by `Keychain` methods. public enum KeychainError: Error { @@ -74,11 +79,12 @@ public class Keychain { - Parameter password: The password to set. - Parameter username: The username to set the password for. - Parameter context: An optional context. + - Parameter label: An optional label. - Returns: The reference to the password. - Throws: `KeychainError.add` if unable to add the password to the keychain. **/ @discardableResult - public func set(password: String, for username: String, context: String? = nil) throws -> Data { + public func set(password: String, for username: String, context: String? = nil, label: String? = nil) throws -> Data { do { let currentPassword = try self.password(for: username, context: context) guard password != currentPassword else { @@ -98,6 +104,7 @@ public class Keychain { var query = [String: Any]() setScope(query: &query, context: context) query[kSecClass as String] = kSecClassGenericPassword + query[kSecAttrLabel as String] = label query[kSecAttrAccount as String] = username query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock query[kSecValueData as String] = password.data(using: .utf8) diff --git a/Sources/TunnelKitManager/MockVPN.swift b/Sources/TunnelKitManager/MockVPN.swift new file mode 100644 index 0000000..93b35df --- /dev/null +++ b/Sources/TunnelKitManager/MockVPN.swift @@ -0,0 +1,74 @@ +// +// MockVPN.swift +// TunnelKit +// +// Created by Davide De Rosa on 6/15/18. +// Copyright (c) 2022 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 + +/// Simulates a VPN provider. +public class MockVPN: VPN { + public init() { + } + + // MARK: VPN + + public func prepare() { + notifyReinstall(false) + notifyStatus(.disconnected) + } + + public func install(_ tunnelBundleIdentifier: String, configuration: NetworkExtensionConfiguration, extra: Data?, completionHandler: ((Result) -> Void)?) { + notifyReinstall(true) + notifyStatus(.disconnected) + completionHandler?(.success(())) + } + + public func reconnect(_ tunnelBundleIdentifier: String, configuration: NetworkExtensionConfiguration, extra: Data?, delay: Double?) { + notifyReinstall(true) + notifyStatus(.connected) + } + + public func disconnect() { + notifyReinstall(false) + notifyStatus(.disconnected) + } + + public func uninstall() { + notifyReinstall(false) + } + + // MARK: Helpers + + private func notifyReinstall(_ isEnabled: Bool) { + var notification = Notification(name: VPNNotification.didReinstall) + notification.vpnIsEnabled = isEnabled + NotificationCenter.default.post(notification) + } + + private func notifyStatus(_ status: VPNStatus) { + var notification = Notification(name: VPNNotification.didChangeStatus) + notification.vpnStatus = status + NotificationCenter.default.post(notification) + } +} diff --git a/Sources/TunnelKitManager/MockVPNProvider.swift b/Sources/TunnelKitManager/MockVPNProvider.swift deleted file mode 100644 index 5cd2a05..0000000 --- a/Sources/TunnelKitManager/MockVPNProvider.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// MockVPNProvider.swift -// TunnelKit -// -// Created by Davide De Rosa on 6/15/18. -// Copyright (c) 2022 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 - -/// Simulates a VPN provider. -public class MockVPNProvider: VPNProvider, VPNProviderIPC { - - public init() { - } - - // MARK: VPNProvider - - public let isPrepared: Bool = true - - public private(set) var isEnabled: Bool = false - - public private(set) var status: VPNStatus = .disconnected - - public func prepare(completionHandler: (() -> Void)?) { - NotificationCenter.default.post(name: VPN.didPrepare, object: nil) - completionHandler?() - } - - public func install(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) { - isEnabled = true - completionHandler?(nil) - } - - public func connect(completionHandler: ((Error?) -> Void)?) { - isEnabled = true - status = .connected - NotificationCenter.default.post(name: VPN.didChangeStatus, object: self) - completionHandler?(nil) - } - - public func disconnect(completionHandler: ((Error?) -> Void)?) { - isEnabled = false - status = .disconnected - NotificationCenter.default.post(name: VPN.didChangeStatus, object: self) - completionHandler?(nil) - } - - public func reconnect(configuration: VPNConfiguration, delay: Double?, completionHandler: ((Error?) -> Void)?) { - isEnabled = true - status = .connected - NotificationCenter.default.post(name: VPN.didChangeStatus, object: self) - completionHandler?(nil) - } - - public func uninstall(completionHandler: (() -> Void)?) { - isEnabled = false - status = .disconnected - NotificationCenter.default.post(name: VPN.didChangeStatus, object: self) - completionHandler?() - } - - // MARK: VPNProviderIPC - - public func requestDebugLog(fallback: (() -> String)?, completionHandler: @escaping (String) -> Void) { - let log = [String](repeating: "lorem ipsum", count: 1000).joined(separator: " ") - completionHandler(log) - } - - public func requestBytesCount(completionHandler: @escaping ((UInt, UInt)?) -> Void) { - completionHandler((0, 0)) - } - - public func requestServerConfiguration(completionHandler: @escaping (Any?) -> Void) { - completionHandler(nil) - } -} diff --git a/Sources/TunnelKitManager/NetworkExtensionConfiguration.swift b/Sources/TunnelKitManager/NetworkExtensionConfiguration.swift new file mode 100644 index 0000000..49022ad --- /dev/null +++ b/Sources/TunnelKitManager/NetworkExtensionConfiguration.swift @@ -0,0 +1,58 @@ +// +// NetworkExtensionConfiguration.swift +// TunnelKit +// +// Created by Davide De Rosa on 9/18/18. +// Copyright (c) 2022 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 + +/// :nodoc: +public struct NetworkExtensionExtra { + public var passwordReference: Data? + + public var onDemandRules: [NEOnDemandRule] = [] + + public var disconnectsOnSleep = false + + public init() { + } +} + +/// Configuration object to feed to a `NetworkExtensionProvider`. +public protocol NetworkExtensionConfiguration { + + /// The profile title in device settings. + var title: String { get } + + /** + Returns a representation for use with tunnel implementations. + + - Parameter bundleIdentifier: The bundle identifier of the tunnel extension. + - Parameter extra: The optional `Extra` arguments. + - Returns An object to use with tunnel implementations. + */ + func asTunnelProtocol( + withBundleIdentifier bundleIdentifier: String, + extra: NetworkExtensionExtra? + ) throws -> NETunnelProviderProtocol +} diff --git a/Sources/TunnelKitManager/NetworkExtensionLocator.swift b/Sources/TunnelKitManager/NetworkExtensionLocator.swift deleted file mode 100644 index d23399e..0000000 --- a/Sources/TunnelKitManager/NetworkExtensionLocator.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// NetworkExtensionLocator.swift -// TunnelKit -// -// Created by Davide De Rosa on 8/25/21. -// Copyright (c) 2022 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 { - - 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/Sources/TunnelKitManager/NetworkExtensionVPN.swift b/Sources/TunnelKitManager/NetworkExtensionVPN.swift new file mode 100644 index 0000000..ece0219 --- /dev/null +++ b/Sources/TunnelKitManager/NetworkExtensionVPN.swift @@ -0,0 +1,295 @@ +// +// NetworkExtensionVPN.swift +// TunnelKit +// +// Created by Davide De Rosa on 6/15/18. +// Copyright (c) 2022 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 + +/// `VPN` based on the NetworkExtension framework. +public class NetworkExtensionVPN: VPN { + + /** + Initializes a provider. + */ + public init() { + 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: VPN + + public func prepare() { + NETunnelProviderManager.loadAllFromPreferences { managers, error in + } + } + + 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( + 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 disconnect() { + lookupAll { + if case .success(let managers) = $0 { + managers.forEach { + $0.connection.stopVPNTunnel() + $0.isOnDemandEnabled = false + $0.isEnabled = false + $0.saveToPreferences(completionHandler: nil) + } + } + } + } + + public func uninstall() { + lookupAll { + if case .success(let managers) = $0 { + managers.forEach { + $0.connection.stopVPNTunnel() + $0.removeFromPreferences(completionHandler: nil) + } + } + } + } + + // MARK: Helpers + + private func install( + _ manager: NETunnelProviderManager, + title: String, + protocolConfiguration: NETunnelProviderProtocol, + onDemandRules: [NEOnDemandRule], + completionHandler: ((Result) -> Void)? + ) { + 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 { + manager.isOnDemandEnabled = false + manager.isEnabled = false + completionHandler?(.failure(error)) + self.notifyError(error) + return + } + manager.loadFromPreferences { error in + if let error = error { + completionHandler?(.failure(error)) + self.notifyError(error) + return + } + completionHandler?(.success(manager)) + self.notifyReinstall(manager) + } + } + } + + private func connect(_ manager: NETunnelProviderManager) { + do { + try manager.connection.startVPNTunnel() + } catch { + notifyError(error) + } + } + + public func lookupAll(completionHandler: @escaping (Result<[NETunnelProviderManager], Error>) -> Void) { + NETunnelProviderManager.loadAllFromPreferences { managers, error in + if let error = error { + completionHandler(.failure(error)) + return + } + completionHandler(.success(managers ?? [])) + } + } + + // MARK: Notifications + + @objc private func vpnDidUpdate(_ notification: Notification) { + guard let connection = notification.object as? NETunnelProviderSession else { + return + } + notifyStatus(connection) + } + + @objc private func vpnDidReinstall(_ notification: Notification) { + guard let manager = notification.object as? NETunnelProviderManager else { + return + } + notifyReinstall(manager) + } + + private func notifyReinstall(_ manager: NETunnelProviderManager) { + let bundleId = manager.tunnelBundleIdentifier + log.debug("VPN did reinstall (\(bundleId ?? "?")): isEnabled=\(manager.isEnabled)") + + var notification = Notification(name: VPNNotification.didReinstall) + notification.vpnBundleIdentifier = bundleId + notification.vpnIsEnabled = manager.isEnabled + NotificationCenter.default.post(notification) + } + + private func notifyStatus(_ connection: NETunnelProviderSession) { + guard let _ = connection.manager.localizedDescription else { + log.verbose("Ignoring VPN notification from bogus manager") + return + } + let bundleId = connection.manager.tunnelBundleIdentifier + log.debug("VPN status did change (\(bundleId ?? "?")): isEnabled=\(connection.manager.isEnabled), status=\(connection.status.rawValue)") + + var notification = Notification(name: VPNNotification.didChangeStatus) + notification.vpnBundleIdentifier = bundleId + notification.vpnIsEnabled = connection.manager.isEnabled + notification.vpnStatus = connection.status.wrappedStatus + NotificationCenter.default.post(notification) + } + + private func notifyError(_ error: Error) { + log.error("VPN command failed: \(error))") + + var notification = Notification(name: VPNNotification.didFail) + notification.vpnError = error + NotificationCenter.default.post(notification) + } +} + +private extension NEVPNManager { + var tunnelBundleIdentifier: String? { + guard let proto = protocolConfiguration as? NETunnelProviderProtocol else { + return nil + } + return proto.providerBundleIdentifier + } + + func isTunnel(withIdentifier bundleIdentifier: String) -> Bool { + return tunnelBundleIdentifier == bundleIdentifier + } +} + +private extension NEVPNStatus { + var wrappedStatus: VPNStatus { + switch self { + case .connected: + return .connected + + case .connecting, .reasserting: + return .connecting + + case .disconnecting: + return .disconnecting + + case .disconnected, .invalid: + return .disconnected + + @unknown default: + return .disconnected + } + } +} diff --git a/Sources/TunnelKitManager/NetworkExtensionVPNConfiguration.swift b/Sources/TunnelKitManager/NetworkExtensionVPNConfiguration.swift deleted file mode 100644 index f3acd9d..0000000 --- a/Sources/TunnelKitManager/NetworkExtensionVPNConfiguration.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// NetworkExtensionVPNConfiguration.swift -// TunnelKit -// -// Created by Davide De Rosa on 8/25/21. -// Copyright (c) 2022 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 { - - public var title: String - - /// The `NEVPNProtocol` object embedding tunnel configuration. - public let protocolConfiguration: NEVPNProtocol - - /// The on-demand rules to establish. - public let onDemandRules: [NEOnDemandRule] - - public init(title: String, protocolConfiguration: NEVPNProtocol, onDemandRules: [NEOnDemandRule]) { - self.title = title - self.protocolConfiguration = protocolConfiguration - self.onDemandRules = onDemandRules - } -} diff --git a/Sources/TunnelKitManager/NetworkExtensionVPNProvider.swift b/Sources/TunnelKitManager/NetworkExtensionVPNProvider.swift deleted file mode 100644 index c3a9fc0..0000000 --- a/Sources/TunnelKitManager/NetworkExtensionVPNProvider.swift +++ /dev/null @@ -1,213 +0,0 @@ -// -// NetworkExtensionVPNProvider.swift -// TunnelKit -// -// Created by Davide De Rosa on 6/15/18. -// Copyright (c) 2022 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, delay: Double? = nil, completionHandler: ((Error?) -> Void)?) { - guard let configuration = configuration as? NetworkExtensionVPNConfiguration else { - fatalError("Not a NetworkExtensionVPNConfiguration") - } - let delay = delay ?? 2.0 - 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() + delay, 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 - - public 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/Sources/TunnelKitManager/VPN.swift b/Sources/TunnelKitManager/VPN.swift index 09dfa36..fcf8a49 100644 --- a/Sources/TunnelKitManager/VPN.swift +++ b/Sources/TunnelKitManager/VPN.swift @@ -2,7 +2,7 @@ // VPN.swift // TunnelKit // -// Created by Davide De Rosa on 6/12/18. +// Created by Davide De Rosa on 9/6/18. // Copyright (c) 2022 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn @@ -25,15 +25,56 @@ import Foundation -/// Wrapper for shared access to VPN-related objects. -public class VPN { +/// Helps controlling a VPN without messing with underlying implementations. +public protocol VPN { + associatedtype Manager - /// The VPN became ready to use. - public static let didPrepare = Notification.Name("VPNDidPrepare") + associatedtype Configuration - /// The VPN did change status. - public static let didChangeStatus = Notification.Name("VPNDidChangeStatus") + associatedtype Extra + + /** + Synchronizes with the current VPN state. + */ + func prepare() + + /** + Installs the VPN profile. - /// The VPN profile did (re)install. - public static let didReinstall = Notification.Name("VPNDidReinstall") + - 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)? + ) + + /** + Reconnects to the 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. + */ + func reconnect( + _ tunnelBundleIdentifier: String, + configuration: Configuration, + extra: Extra?, + delay: Double? + ) + + /** + Disconnects from the VPN. + */ + func disconnect() + + /** + Uninstalls the VPN profile. + */ + func uninstall() } diff --git a/Sources/TunnelKitManager/VPNNotification.swift b/Sources/TunnelKitManager/VPNNotification.swift new file mode 100644 index 0000000..6074d9f --- /dev/null +++ b/Sources/TunnelKitManager/VPNNotification.swift @@ -0,0 +1,102 @@ +// +// VPNNotification.swift +// TunnelKit +// +// Created by Davide De Rosa on 6/12/18. +// Copyright (c) 2022 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 + +/// VPN notifications. +public struct VPNNotification { + + /// The VPN did reinstall. + public static let didReinstall = Notification.Name("VPNDidReinstall") + + /// The VPN did change its status. + public static let didChangeStatus = Notification.Name("VPNDidChangeStatus") + + /// The VPN triggered some error. + public static let didFail = Notification.Name("VPNDidFail") +} + +extension Notification { + + /// The VPN bundle identifier. + public var vpnBundleIdentifier: String? { + get { + guard let vpnBundleIdentifier = userInfo?["BundleIdentifier"] as? String else { + fatalError("Notification has no vpnBundleIdentifier") + } + return vpnBundleIdentifier + } + set { + var newInfo = userInfo ?? [:] + newInfo["BundleIdentifier"] = newValue + userInfo = newInfo + } + } + + /// The current VPN enabled state. + public var vpnIsEnabled: Bool { + get { + guard let vpnIsEnabled = userInfo?["IsEnabled"] as? Bool else { + fatalError("Notification has no vpnIsEnabled") + } + return vpnIsEnabled + } + set { + var newInfo = userInfo ?? [:] + newInfo["IsEnabled"] = newValue + userInfo = newInfo + } + } + + /// The current VPN status. + public var vpnStatus: VPNStatus { + get { + guard let vpnStatus = userInfo?["Status"] as? VPNStatus else { + fatalError("Notification has no vpnStatus") + } + return vpnStatus + } + set { + var newInfo = userInfo ?? [:] + newInfo["Status"] = newValue + userInfo = newInfo + } + } + + /// The triggered VPN error. + public var vpnError: Error { + get { + guard let vpnError = userInfo?["Error"] as? Error else { + fatalError("Notification has no vpnError") + } + return vpnError + } + set { + var newInfo = userInfo ?? [:] + newInfo["Error"] = newValue + userInfo = newInfo + } + } +} diff --git a/Sources/TunnelKitManager/VPNProvider.swift b/Sources/TunnelKitManager/VPNProvider.swift deleted file mode 100644 index f0638d9..0000000 --- a/Sources/TunnelKitManager/VPNProvider.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// VPNProvider.swift -// TunnelKit -// -// Created by Davide De Rosa on 9/6/18. -// Copyright (c) 2022 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 - -/// Helps controlling a VPN without messing with underlying implementations. -public protocol VPNProvider: AnyObject { - - /// `true` if the VPN is ready for use. - var isPrepared: Bool { get } - - /// `true` if the associated VPN profile is enabled. - var isEnabled: Bool { get } - - /// The status of the VPN. - var status: VPNStatus { get } - - /** - Prepares the VPN for use. - - - Postcondition: The VPN is ready to use and `isPrepared` becomes `true`. - - Parameter completionHandler: The completion handler. - - Seealso: `isPrepared` - */ - func prepare(completionHandler: (() -> Void)?) - - /** - Installs the VPN profile. - - - Parameter configuration: The `VPNConfiguration` to install. - - Parameter completionHandler: The completion handler with an optional error. - */ - func install(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) - - /** - Connects to the VPN. - - - Parameter completionHandler: The completion handler with an optional error. - */ - func connect(completionHandler: ((Error?) -> Void)?) - - /** - Disconnects from the VPN. - - - Parameter completionHandler: The completion handler with an optional error. - */ - func disconnect(completionHandler: ((Error?) -> Void)?) - - /** - Reconnects to the VPN. - - - Parameter configuration: The `VPNConfiguration` to install. - - Parameter delay: The reconnection delay in seconds. - - Parameter completionHandler: The completion handler with an optional error. - */ - func reconnect(configuration: VPNConfiguration, delay: Double?, completionHandler: ((Error?) -> Void)?) - - /** - Uninstalls the VPN profile. - - - Parameter completionHandler: The completion handler. - */ - func uninstall(completionHandler: (() -> Void)?) -} diff --git a/Sources/TunnelKitManager/VPNProviderIPC.swift b/Sources/TunnelKitManager/VPNProviderIPC.swift deleted file mode 100644 index c98fcc3..0000000 --- a/Sources/TunnelKitManager/VPNProviderIPC.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// VPNProviderIPC.swift -// TunnelKit -// -// Created by Davide De Rosa on 8/25/21. -// Copyright (c) 2022 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/Sources/TunnelKitManager/VPNStatus.swift b/Sources/TunnelKitManager/VPNStatus.swift index 2c06b05..c504301 100644 --- a/Sources/TunnelKitManager/VPNStatus.swift +++ b/Sources/TunnelKitManager/VPNStatus.swift @@ -25,7 +25,7 @@ import Foundation -/// Status of a `VPNProvider`. +/// Status of a `VPN`. public enum VPNStatus: String { /// VPN is connected. diff --git a/Sources/TunnelKitOpenVPNAppExtension/OpenVPNTunnelProvider.swift b/Sources/TunnelKitOpenVPNAppExtension/OpenVPNTunnelProvider.swift index 927e20f..3bd9a7f 100644 --- a/Sources/TunnelKitOpenVPNAppExtension/OpenVPNTunnelProvider.swift +++ b/Sources/TunnelKitOpenVPNAppExtension/OpenVPNTunnelProvider.swift @@ -48,6 +48,7 @@ import TunnelKitOpenVPNManager import TunnelKitOpenVPNProtocol import TunnelKitAppExtension import CTunnelKitCore +import __TunnelKitUtils private let log = SwiftyBeaver.self @@ -106,9 +107,7 @@ open class OpenVPNTunnelProvider: NEPacketTunnelProvider { private let prngSeedLength = 64 private var cachesURL: URL { - guard let appGroup = appGroup else { - fatalError("Accessing cachesURL before parsing app group") - } + let appGroup = cfg.appGroup guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { fatalError("No access to app group: \(appGroup)") } @@ -117,11 +116,7 @@ open class OpenVPNTunnelProvider: NEPacketTunnelProvider { // MARK: Tunnel configuration - private var appGroup: String! - - private lazy var defaults = UserDefaults(suiteName: appGroup) - - private var cfg: OpenVPNProvider.Configuration! + private var cfg: OpenVPN.ProviderConfiguration! private var strategy: ConnectionStrategy! @@ -160,8 +155,7 @@ open class OpenVPNTunnelProvider: NEPacketTunnelProvider { guard let providerConfiguration = tunnelProtocol.providerConfiguration else { throw OpenVPNProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration") } - try appGroup = OpenVPNProvider.Configuration.appGroup(from: providerConfiguration) - try cfg = OpenVPNProvider.Configuration.parsed(from: providerConfiguration) + cfg = try fromDictionary(OpenVPN.ProviderConfiguration.self, providerConfiguration) } catch let e { var message: String? if let te = e as? OpenVPNProviderConfigurationError { @@ -179,7 +173,7 @@ open class OpenVPNTunnelProvider: NEPacketTunnelProvider { } // prepare for logging (append) - if let content = cfg.existingLog(in: appGroup) { + if let content = cfg.debugLog { var existingLog = content.components(separatedBy: "\n") if let i = existingLog.firstIndex(of: logSeparator) { existingLog.removeFirst(i + 2) @@ -198,9 +192,7 @@ open class OpenVPNTunnelProvider: NEPacketTunnelProvider { // logging only ACTIVE from now on // override library configuration - if let masksPrivateData = cfg.masksPrivateData { - CoreConfiguration.masksPrivateData = masksPrivateData - } + CoreConfiguration.masksPrivateData = cfg.masksPrivateData if let versionIdentifier = cfg.versionIdentifier { CoreConfiguration.versionIdentifier = versionIdentifier } @@ -218,21 +210,24 @@ open class OpenVPNTunnelProvider: NEPacketTunnelProvider { } log.info("Starting tunnel...") - cfg.clearLastError(in: appGroup) + cfg.lastError = nil guard OpenVPN.prepareRandomNumberGenerator(seedLength: prngSeedLength) else { completionHandler(OpenVPNProviderConfigurationError.prngInitialization) return } - cfg.print(appVersion: appVersion) + if let appVersion = appVersion { + log.info("App version: \(appVersion)") + } + cfg.print() // prepare to pick endpoints - strategy = ConnectionStrategy(configuration: cfg.sessionConfiguration) + strategy = ConnectionStrategy(configuration: cfg.configuration) let session: OpenVPNSession do { - session = try OpenVPNSession(queue: tunnelQueue, configuration: cfg.sessionConfiguration, cachesURL: cachesURL) + session = try OpenVPNSession(queue: tunnelQueue, configuration: cfg.configuration, cachesURL: cachesURL) refreshDataCount() } catch let e { completionHandler(e) @@ -253,7 +248,7 @@ open class OpenVPNTunnelProvider: NEPacketTunnelProvider { open override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { pendingStartHandler = nil log.info("Stopping tunnel...") - cfg.clearLastError(in: appGroup) + cfg.lastError = nil guard let session = session else { flushLog() @@ -280,31 +275,6 @@ open class OpenVPNTunnelProvider: NEPacketTunnelProvider { } } - open override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { - var response: Data? - switch OpenVPNProvider.Message(messageData) { - case .requestLog: - response = memoryLog.description.data(using: .utf8) - - case .dataCount: - if let session = session, let dataCount = session.dataCount() { - response = Data() - response?.append(UInt64(dataCount.0)) // inbound - response?.append(UInt64(dataCount.1)) // outbound - } - - case .serverConfiguration: - if let cfg = session?.serverConfiguration() as? OpenVPN.Configuration { - let encoder = JSONEncoder() - response = try? encoder.encode(cfg) - } - - default: - break - } - completionHandler?(response) - } - // MARK: Wake/Sleep (debugging placeholders) open override func wake() { @@ -348,7 +318,7 @@ open class OpenVPNTunnelProvider: NEPacketTunnelProvider { private func connectTunnel(via socket: GenericSocket) { log.info("Will connect to \(socket)") - cfg.clearLastError(in: appGroup) + cfg.lastError = nil log.debug("Socket type is \(type(of: socket))") self.socket = socket @@ -421,10 +391,10 @@ open class OpenVPNTunnelProvider: NEPacketTunnelProvider { self?.refreshDataCount() } guard isCountingData, let session = session, let dataCount = session.dataCount() else { - defaults?.removeDataCountArray() + cfg.dataCount = nil return } - defaults?.dataCountArray = [dataCount.0, dataCount.1] + cfg.dataCount = dataCount } } @@ -451,10 +421,10 @@ extension OpenVPNTunnelProvider: GenericSocketDelegate { return } if session.canRebindLink() { - session.rebindLink(producer.link(xorMask: cfg.sessionConfiguration.xorMask)) + session.rebindLink(producer.link(xorMask: cfg.configuration.xorMask)) reasserting = false } else { - session.setLink(producer.link(xorMask: cfg.sessionConfiguration.xorMask)) + session.setLink(producer.link(xorMask: cfg.configuration.xorMask)) } } @@ -562,6 +532,8 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { } } + cfg.serverConfiguration = session.serverConfiguration() as? OpenVPN.Configuration + bringNetworkUp(remoteAddress: remoteAddress, localOptions: session.configuration, options: options) { (error) in // FIXME: XPC queue @@ -588,6 +560,8 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { } public func sessionDidStop(_: OpenVPNSession, withError error: Error?, shouldReconnect: Bool) { + cfg.serverConfiguration = nil + if let error = error { log.error("Session did stop with error: \(error)") } else { @@ -681,10 +655,10 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { var dnsServers: [String] = [] var dnsSettings: NEDNSSettings? if #available(iOS 14, macOS 11, *) { - switch cfg.sessionConfiguration.dnsProtocol { + switch cfg.configuration.dnsProtocol { case .https: - dnsServers = cfg.sessionConfiguration.dnsServers ?? [] - guard let serverURL = cfg.sessionConfiguration.dnsHTTPSURL else { + dnsServers = cfg.configuration.dnsServers ?? [] + guard let serverURL = cfg.configuration.dnsHTTPSURL else { break } let specific = NEDNSOverHTTPSSettings(servers: dnsServers) @@ -694,11 +668,11 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { log.info("\tHTTPS URL: \(serverURL.maskedDescription)") case .tls: - guard let dnsServers = cfg.sessionConfiguration.dnsServers else { + guard let dnsServers = cfg.configuration.dnsServers else { session?.shutdown(error: OpenVPNProviderError.dnsFailure) return } - guard let serverName = cfg.sessionConfiguration.dnsTLSServerName else { + guard let serverName = cfg.configuration.dnsTLSServerName else { break } let specific = NEDNSOverTLSSettings(servers: dnsServers) @@ -715,7 +689,7 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { // fall back if dnsSettings == nil { dnsServers = [] - if let servers = cfg.sessionConfiguration.dnsServers, + if let servers = cfg.configuration.dnsServers, !servers.isEmpty { dnsServers = servers } else if let servers = options.dnsServers { @@ -736,7 +710,7 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { dnsSettings?.matchDomains = [""] } - if let searchDomains = cfg.sessionConfiguration.searchDomains ?? options.searchDomains { + if let searchDomains = cfg.configuration.searchDomains ?? options.searchDomains { log.info("DNS: Using search domains \(searchDomains.maskedDescription)") dnsSettings?.domainName = searchDomains.first dnsSettings?.searchDomains = searchDomains @@ -757,13 +731,13 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { } var proxySettings: NEProxySettings? - if let httpsProxy = cfg.sessionConfiguration.httpsProxy ?? options.httpsProxy { + if let httpsProxy = cfg.configuration.httpsProxy ?? options.httpsProxy { proxySettings = NEProxySettings() proxySettings?.httpsServer = httpsProxy.neProxy() proxySettings?.httpsEnabled = true log.info("Routing: Setting HTTPS proxy \(httpsProxy.address.maskedDescription):\(httpsProxy.port)") } - if let httpProxy = cfg.sessionConfiguration.httpProxy ?? options.httpProxy { + if let httpProxy = cfg.configuration.httpProxy ?? options.httpProxy { if proxySettings == nil { proxySettings = NEProxySettings() } @@ -771,7 +745,7 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { proxySettings?.httpEnabled = true log.info("Routing: Setting HTTP proxy \(httpProxy.address.maskedDescription):\(httpProxy.port)") } - if let pacURL = cfg.sessionConfiguration.proxyAutoConfigurationURL ?? options.proxyAutoConfigurationURL { + if let pacURL = cfg.configuration.proxyAutoConfigurationURL ?? options.proxyAutoConfigurationURL { if proxySettings == nil { proxySettings = NEProxySettings() } @@ -781,7 +755,7 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { } // only set if there is a proxy (proxySettings set to non-nil above) - if let bypass = cfg.sessionConfiguration.proxyBypassDomains ?? options.proxyBypassDomains { + if let bypass = cfg.configuration.proxyBypassDomains ?? options.proxyBypassDomains { proxySettings?.exceptionList = bypass log.info("Routing: Setting proxy by-pass list: \(bypass.maskedDescription)") } @@ -828,7 +802,7 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { newSettings.ipv6Settings = ipv6Settings newSettings.dnsSettings = dnsSettings newSettings.proxySettings = proxySettings - if let mtu = cfg.sessionConfiguration.mtu { + if let mtu = cfg.configuration.mtu { newSettings.mtu = NSNumber(value: mtu) } @@ -868,7 +842,7 @@ extension OpenVPNTunnelProvider { private func flushLog() { log.debug("Flushing log...") - if let url = cfg.urlForLog(in: appGroup) { + if let url = cfg.urlForDebugLog { memoryLog.flush(to: url) } } @@ -891,7 +865,7 @@ extension OpenVPNTunnelProvider { // MARK: Errors private func setErrorStatus(with error: Error) { - defaults?.set(unifiedError(from: error).rawValue, forKey: OpenVPNProvider.Configuration.lastErrorKey) + cfg.lastError = unifiedError(from: error) } private func unifiedError(from error: Error) -> OpenVPNProviderError { diff --git a/Sources/TunnelKitOpenVPNManager/OpenVPN+ProviderConfiguration.swift b/Sources/TunnelKitOpenVPNManager/OpenVPN+ProviderConfiguration.swift new file mode 100644 index 0000000..3934825 --- /dev/null +++ b/Sources/TunnelKitOpenVPNManager/OpenVPN+ProviderConfiguration.swift @@ -0,0 +1,281 @@ +// +// OpenVPN+ProviderConfiguration.swift +// TunnelKit +// +// Created by Davide De Rosa on 3/6/22. +// Copyright (c) 2022 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 TunnelKitManager +import TunnelKitCore +import TunnelKitOpenVPNCore +import NetworkExtension +import SwiftyBeaver +import __TunnelKitUtils + +private let log = SwiftyBeaver.self + +extension OpenVPN { + + /// Specific configuration for OpenVPN. + public struct ProviderConfiguration: Codable { + fileprivate enum Filenames: String { + case debugLog = "OpenVPN.Tunnel.log" + } + + fileprivate enum Keys: String { + case dataCount = "OpenVPN.DataCount" + + case serverConfiguration = "OpenVPN.ServerConfiguration" + + case lastError = "OpenVPN.LastError" + } + + /// Optional version identifier about the client pushed to server in peer-info as `IV_UI_VER`. + public var versionIdentifier: String? + + /// The configuration title. + public let title: String + + /// The access group for shared data. + public let appGroup: String + + /// The client configuration. + public let configuration: OpenVPN.Configuration + + /// The optional username. + public var username: String? + + /// Enables debugging. + public var shouldDebug = false + + /// Optional debug log format (SwiftyBeaver format). + public var debugLogFormat: String? = nil + + /// Mask private data in debug log (default is `true`). + public var masksPrivateData = true + + /// :nodoc: + public init(_ title: String, appGroup: String, configuration: OpenVPN.Configuration) { + self.title = title + self.appGroup = appGroup + self.configuration = configuration + } + + /// :nodoc: + public func print() { + if let versionIdentifier = versionIdentifier { + log.info("Tunnel version: \(versionIdentifier)") + } + configuration.print() + log.info("Debug: \(shouldDebug)") + log.info("Masks private data: \(masksPrivateData)") + } + } +} + +// MARK: NetworkExtensionConfiguration + +extension OpenVPN.ProviderConfiguration: NetworkExtensionConfiguration { + + /// :nodoc: + public func asTunnelProtocol( + withBundleIdentifier tunnelBundleIdentifier: String, + extra: NetworkExtensionExtra? + ) throws -> NETunnelProviderProtocol { + guard let firstRemote = configuration.remotes?.first else { + preconditionFailure("No remotes set") + } + + let protocolConfiguration = NETunnelProviderProtocol() + protocolConfiguration.providerBundleIdentifier = tunnelBundleIdentifier + protocolConfiguration.serverAddress = "\(firstRemote.address):\(firstRemote.proto.port)" + if let username = username { + protocolConfiguration.username = username + protocolConfiguration.passwordReference = extra?.passwordReference + } + protocolConfiguration.disconnectOnSleep = extra?.disconnectsOnSleep ?? false + protocolConfiguration.providerConfiguration = try asDictionary() + return protocolConfiguration + } +} + +// MARK: Shared data + +extension OpenVPN.ProviderConfiguration { + + /** + The most recent (received, sent) count in bytes. + */ + public var dataCount: DataCount? { + get { + return defaults?.openVPNDataCount + } + set { + defaults?.openVPNDataCount = newValue + } + } + + /** + The server configuration pulled by the VPN. + */ + public var serverConfiguration: OpenVPN.Configuration? { + get { + return defaults?.openVPNServerConfiguration + } + set { + defaults?.openVPNServerConfiguration = newValue + } + } + + /** + The last error reported by the tunnel, if any. + */ + public var lastError: OpenVPNProviderError? { + get { + return defaults?.openVPNLastError + } + set { + defaults?.openVPNLastError = newValue + } + } + + /** + The URL of the latest debug log. + */ + public var urlForDebugLog: URL? { + return FileManager.default.openVPNURLForDebugLog(appGroup: appGroup) + } + + /** + The content of the latest debug log. + */ + public var debugLog: String? { + return FileManager.default.openVPNDebugLog(appGroup: appGroup) + } + + private var defaults: UserDefaults? { + return UserDefaults(suiteName: appGroup) + } +} + +/// :nodoc: +extension UserDefaults { + public var openVPNDataCount: DataCount? { + get { + guard let rawValue = openVPNDataCountArray else { + return nil + } + guard rawValue.count == 2 else { + return nil + } + return DataCount(rawValue[0], rawValue[1]) + } + set { + guard let newValue = newValue else { + openVPNRemoveDataCountArray() + return + } + openVPNDataCountArray = [newValue.received, newValue.sent] + } + } + + @objc private var openVPNDataCountArray: [UInt]? { + get { + return array(forKey: OpenVPN.ProviderConfiguration.Keys.dataCount.rawValue) as? [UInt] + } + set { + set(newValue, forKey: OpenVPN.ProviderConfiguration.Keys.dataCount.rawValue) + } + } + + private func openVPNRemoveDataCountArray() { + removeObject(forKey: OpenVPN.ProviderConfiguration.Keys.dataCount.rawValue) + } + + public var openVPNServerConfiguration: OpenVPN.Configuration? { + get { + guard let raw = data(forKey: OpenVPN.ProviderConfiguration.Keys.serverConfiguration.rawValue) else { + return nil + } + let decoder = JSONDecoder() + do { + let cfg = try decoder.decode(OpenVPN.Configuration.self, from: raw) + return cfg + } catch { + log.error("Unable to decode server configuration: \(error)") + return nil + } + } + set { + guard let newValue = newValue else { + return + } + let encoder = JSONEncoder() + do { + let raw = try encoder.encode(newValue) + set(raw, forKey: OpenVPN.ProviderConfiguration.Keys.serverConfiguration.rawValue) + } catch { + log.error("Unable to encode server configuration: \(error)") + } + } + } + + public var openVPNLastError: OpenVPNProviderError? { + get { + guard let rawValue = string(forKey: OpenVPN.ProviderConfiguration.Keys.lastError.rawValue) else { + return nil + } + return OpenVPNProviderError(rawValue: rawValue) + } + set { + guard let newValue = newValue else { + removeObject(forKey: OpenVPN.ProviderConfiguration.Keys.lastError.rawValue) + return + } + set(newValue.rawValue, forKey: OpenVPN.ProviderConfiguration.Keys.lastError.rawValue) + } + } +} + +/// :nodoc: +extension FileManager { + public func openVPNURLForDebugLog(appGroup: String) -> URL? { + return documentsURL(appGroup: appGroup)? + .appendingPathComponent(OpenVPN.ProviderConfiguration.Filenames.debugLog.rawValue) + } + + public func openVPNDebugLog(appGroup: String) -> String? { + guard let url = openVPNURLForDebugLog(appGroup: appGroup) else { + return nil + } + do { + return try String(contentsOf: url) + } catch { + log.error("Unable to access debug log: \(error)") + return nil + } + } + + private func documentsURL(appGroup: String) -> URL? { + return containerURL(forSecurityApplicationGroupIdentifier: appGroup) + } +} diff --git a/Sources/TunnelKitOpenVPNManager/OpenVPNProvider+Configuration.swift b/Sources/TunnelKitOpenVPNManager/OpenVPNProvider+Configuration.swift deleted file mode 100644 index b70e720..0000000 --- a/Sources/TunnelKitOpenVPNManager/OpenVPNProvider+Configuration.swift +++ /dev/null @@ -1,327 +0,0 @@ -// -// OpenVPNProvider+Configuration.swift -// TunnelKit -// -// Created by Davide De Rosa on 10/23/17. -// Copyright (c) 2022 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 . -// -// This file incorporates work covered by the following copyright and -// permission notice: -// -// Copyright (c) 2018-Present Private Internet Access -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -import Foundation -import NetworkExtension -import SwiftyBeaver -import TunnelKitManager -import TunnelKitOpenVPNCore -import __TunnelKitUtils - -private let log = SwiftyBeaver.self - -extension OpenVPNProvider { - private struct ExtraKeys { - static let appGroup = "appGroup" - } - - // MARK: Configuration - - /// The way to create a `OpenVPNProvider.Configuration` object for the tunnel profile. - public struct ConfigurationBuilder { - - public static let defaults = Configuration( - sessionConfiguration: OpenVPN.ConfigurationBuilder().build(), - shouldDebug: false, - debugLogFormat: nil, - masksPrivateData: true, - versionIdentifier: nil - ) - - /// The session configuration. - public var sessionConfiguration: OpenVPN.Configuration - - // MARK: Debugging - - /// Enables debugging. - public var shouldDebug: Bool - - /// Optional debug log format (SwiftyBeaver format). - public var debugLogFormat: String? - - /// Mask private data in debug log (default is `true`). - public var masksPrivateData: Bool? - - /// Optional version identifier about the client pushed to server in peer-info as `IV_UI_VER`. - public var versionIdentifier: String? - - // MARK: Building - - /** - Default initializer. - - - Parameter ca: The CA certificate. - */ - public init(sessionConfiguration: OpenVPN.Configuration) { - self.sessionConfiguration = sessionConfiguration - shouldDebug = ConfigurationBuilder.defaults.shouldDebug - debugLogFormat = ConfigurationBuilder.defaults.debugLogFormat - masksPrivateData = ConfigurationBuilder.defaults.masksPrivateData - versionIdentifier = ConfigurationBuilder.defaults.versionIdentifier - } - - /** - Builds a `OpenVPNProvider.Configuration` object that will connect to the provided endpoint. - - - Returns: A `OpenVPNProvider.Configuration` object with this builder and the additional method parameters. - */ - public func build() -> Configuration { - return Configuration( - sessionConfiguration: sessionConfiguration, - shouldDebug: shouldDebug, - debugLogFormat: shouldDebug ? debugLogFormat : nil, - masksPrivateData: masksPrivateData, - versionIdentifier: versionIdentifier - ) - } - } - - /// Offers a bridge between the abstract `OpenVPNProvider.ConfigurationBuilder` and a concrete `NETunnelProviderProtocol` profile. - public struct Configuration: Codable { - - /// - Seealso: `OpenVPNProvider.ConfigurationBuilder.sessionConfiguration` - public let sessionConfiguration: OpenVPN.Configuration - - /// - Seealso: `OpenVPNProvider.ConfigurationBuilder.shouldDebug` - public let shouldDebug: Bool - - /// - Seealso: `OpenVPNProvider.ConfigurationBuilder.debugLogFormat` - public let debugLogFormat: String? - - /// - Seealso: `OpenVPNProvider.ConfigurationBuilder.masksPrivateData` - public let masksPrivateData: Bool? - - /// - Seealso: `OpenVPNProvider.ConfigurationBuilder.versionIdentifier` - public let versionIdentifier: String? - - // MARK: Shortcuts - - static let debugLogFilename = "debug.log" - - public static let lastErrorKey = "TunnelKitLastError" - - fileprivate static let dataCountKey = "TunnelKitDataCount" - - /** - Returns the URL of the latest debug log. - - - Parameter in: The app group where to locate the log file. - - Returns: The URL of the debug log, if any. - */ - public func urlForLog(in appGroup: String) -> URL? { - guard shouldDebug else { - return nil - } - guard let parentURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { - return nil - } - return parentURL.appendingPathComponent(Configuration.debugLogFilename) - } - - /** - Returns the content of the latest debug log. - - - Parameter in: The app group where to locate the log file. - - Returns: The content of the debug log, if any. - */ - public func existingLog(in appGroup: String) -> String? { - guard let url = urlForLog(in: appGroup) else { - return nil - } - return try? String(contentsOf: url) - } - - /** - Returns the last error reported by the tunnel, if any. - - - Parameter in: The app group where to locate the error key. - - Returns: The last tunnel error, if any. - */ - public func lastError(in appGroup: String) -> OpenVPNProviderError? { - guard let rawValue = UserDefaults(suiteName: appGroup)?.string(forKey: Configuration.lastErrorKey) else { - return nil - } - return OpenVPNProviderError(rawValue: rawValue) - } - - /** - Clear the last error status. - - - Parameter in: The app group where to locate the error key. - */ - public func clearLastError(in appGroup: String) { - UserDefaults(suiteName: appGroup)?.removeObject(forKey: Configuration.lastErrorKey) - } - - /** - Returns the most recent (received, sent) count in bytes. - - - Parameter in: The app group where to locate the count pair. - - Returns: The bytes count pair, if any. - */ - public func dataCount(in appGroup: String) -> (Int, Int)? { - guard let rawValue = UserDefaults(suiteName: appGroup)?.dataCountArray else { - return nil - } - guard rawValue.count == 2 else { - return nil - } - return (rawValue[0], rawValue[1]) - } - - // MARK: API - - /** - Parses the app group from a provider configuration map. - - - Parameter from: The map to parse. - - Returns: The parsed app group. - - Throws: `OpenVPNProviderError.configuration` if `providerConfiguration` does not contain an app group. - */ - public static func appGroup(from providerConfiguration: [String: Any]) throws -> String { - guard let appGroup = providerConfiguration[ExtraKeys.appGroup] as? String else { - throw OpenVPNProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(ExtraKeys.appGroup)]") - } - return appGroup - } - - /** - Parses a new `OpenVPNProvider.Configuration` object from a provider configuration map. - - - Parameter from: The map to parse. - - Returns: The parsed `OpenVPNProvider.Configuration` object. - - Throws: `OpenVPNProviderError.configuration` if `providerConfiguration` is incomplete. - */ - public static func parsed(from providerConfiguration: [String: Any]) throws -> Configuration { - return try fromDictionary(OpenVPNProvider.Configuration.self, providerConfiguration) - } - - /** - Returns a dictionary representation of this configuration for use with `NETunnelProviderProtocol.providerConfiguration`. - - - Parameter appGroup: The name of the app group in which the tunnel extension lives in. - - Returns: The dictionary representation of `self`. - */ - public func generatedProviderConfiguration(appGroup: String) -> [String: Any] { - do { - var dict = try asDictionary() - dict[ExtraKeys.appGroup] = appGroup - return dict - } catch let e { - log.error("Unable to encode OpenVPN.Configuration: \(e)") - } - return [:] - } - - /** - Generates a `NETunnelProviderProtocol` from this configuration. - - - Parameter bundleIdentifier: The provider bundle identifier required to locate the tunnel extension. - - Parameter appGroup: The name of the app group in which the tunnel extension lives in. - - Parameter context: The keychain context where to look for the password reference. - - Parameter credentials: The credentials to authenticate with. - - Returns: The generated `NETunnelProviderProtocol` object. - - Throws: `OpenVPNProviderError.credentials` if unable to store `credentials.password` to the `appGroup` keychain. - */ - public func generatedTunnelProtocol( - withBundleIdentifier bundleIdentifier: String, - appGroup: String, - context: String, - credentials: OpenVPN.Credentials?) throws -> NETunnelProviderProtocol - { - let protocolConfiguration = NETunnelProviderProtocol() - let keychain = Keychain(group: appGroup) - - protocolConfiguration.providerBundleIdentifier = bundleIdentifier - guard let firstRemote = sessionConfiguration.remotes?.first else { - fatalError("No remotes set") - } - protocolConfiguration.serverAddress = "\(firstRemote.address):\(firstRemote.proto.port)" - if let username = credentials?.username { - protocolConfiguration.username = username - if let password = credentials?.password { - protocolConfiguration.passwordReference = try? keychain.set(password: password, for: username, context: context) - } - } - protocolConfiguration.providerConfiguration = generatedProviderConfiguration(appGroup: appGroup) - - return protocolConfiguration - } - - public func print(appVersion: String?) { - if let appVersion = appVersion { - log.info("App version: \(appVersion)") - } - sessionConfiguration.print() - log.info("\tDebug: \(shouldDebug)") - log.info("\tMasks private data: \(masksPrivateData ?? true)") - } - } -} - -// MARK: Modification - -extension OpenVPNProvider.Configuration { - - /** - Returns a `OpenVPNProvider.ConfigurationBuilder` to use this configuration as a starting point for a new one. - - - Returns: An editable `OpenVPNProvider.ConfigurationBuilder` initialized with this configuration. - */ - public func builder() -> OpenVPNProvider.ConfigurationBuilder { - var builder = OpenVPNProvider.ConfigurationBuilder(sessionConfiguration: sessionConfiguration) - builder.shouldDebug = shouldDebug - builder.debugLogFormat = debugLogFormat - builder.masksPrivateData = masksPrivateData - builder.versionIdentifier = versionIdentifier - return builder - } -} - -public extension UserDefaults { - @objc var dataCountArray: [Int]? { - get { - return array(forKey: OpenVPNProvider.Configuration.dataCountKey) as? [Int] - } - set { - set(newValue, forKey: OpenVPNProvider.Configuration.dataCountKey) - } - } - - func removeDataCountArray() { - removeObject(forKey: OpenVPNProvider.Configuration.dataCountKey) - } -} diff --git a/Sources/TunnelKitOpenVPNManager/OpenVPNProvider+Interaction.swift b/Sources/TunnelKitOpenVPNManager/OpenVPNProvider+Interaction.swift deleted file mode 100644 index b47b850..0000000 --- a/Sources/TunnelKitOpenVPNManager/OpenVPNProvider+Interaction.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// OpenVPNProvider+Interaction.swift -// TunnelKit -// -// Created by Davide De Rosa on 9/24/17. -// Copyright (c) 2022 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 . -// -// This file incorporates work covered by the following copyright and -// permission notice: -// - -import Foundation - -extension OpenVPNProvider { - - /// The messages accepted by `OpenVPNProvider`. - public class Message: Equatable { - - /// Requests a snapshot of the latest debug log. Returns the log data decoded from UTF-8. - public static let requestLog = Message(0xff) - - /// Requests the current bytes count from data channel (if connected). - /// - /// Data is 16 bytes: low 8 = received, high 8 = sent. - public static let dataCount = Message(0xfe) - - /// Requests the configuration pulled from the server (if connected and available). - /// - /// Data is JSON (Decodable). - public static let serverConfiguration = Message(0xfd) - - /// The underlying raw message `Data` to forward to the tunnel via IPC. - public let data: Data - - private init(_ byte: UInt8) { - data = Data([byte]) - } - - public init(_ data: Data) { - self.data = data - } - - // MARK: Equatable - - public static func ==(lhs: Message, rhs: Message) -> Bool { - return (lhs.data == rhs.data) - } - } -} diff --git a/Sources/TunnelKitOpenVPNManager/OpenVPNProvider.swift b/Sources/TunnelKitOpenVPNManager/OpenVPNProvider.swift deleted file mode 100644 index 758860a..0000000 --- a/Sources/TunnelKitOpenVPNManager/OpenVPNProvider.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// OpenVPNProvider.swift -// TunnelKit -// -// Created by Davide De Rosa on 6/15/18. -// Copyright (c) 2022 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 TunnelKitManager -import TunnelKitOpenVPNCore - -/// `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) { - provider = NetworkExtensionVPNProvider(locator: NetworkExtensionTunnelLocator(bundleIdentifier: bundleIdentifier)) - } - - // 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, delay: Double? = nil, completionHandler: ((Error?) -> Void)?) { - provider.reconnect(configuration: configuration, delay: delay, completionHandler: completionHandler) - } - - public func uninstall(completionHandler: (() -> Void)?) { - provider.uninstall(completionHandler: completionHandler) - } - - // MARK: VPNProviderIPC - - public func requestDebugLog(fallback: (() -> String)?, completionHandler: @escaping (String) -> Void) { - guard provider.status != .disconnected else { - completionHandler(fallback?() ?? "") - return - } - findAndRequestDebugLog { (recent) in - DispatchQueue.main.async { - guard let recent = recent else { - completionHandler(fallback?() ?? "") - return - } - completionHandler(recent) - } - } - } - - public func requestBytesCount(completionHandler: @escaping ((UInt, UInt)?) -> Void) { - provider.lookup { manager, error in - guard let session = manager?.connection as? NETunnelProviderSession else { - DispatchQueue.main.async { - completionHandler(nil) - } - return - } - do { - try session.sendProviderMessage(Message.dataCount.data) { (data) in - guard let data = data, data.count == 16 else { - DispatchQueue.main.async { - completionHandler(nil) - } - return - } - let bytesIn: UInt = data.subdata(in: 0..<8).withUnsafeBytes { $0.load(as: UInt.self) } - let bytesOut: UInt = data.subdata(in: 8..<16).withUnsafeBytes { $0.load(as: UInt.self) } - DispatchQueue.main.async { - completionHandler((bytesIn, bytesOut)) - } - } - } catch { - DispatchQueue.main.async { - completionHandler(nil) - } - } - } - } - - public func requestServerConfiguration(completionHandler: @escaping (Any?) -> Void) { - provider.lookup { manager, error in - guard let session = manager?.connection as? NETunnelProviderSession else { - DispatchQueue.main.async { - completionHandler(nil) - } - return - } - do { - try session.sendProviderMessage(Message.serverConfiguration.data) { (data) in - guard let data = data, let cfg = try? JSONDecoder().decode(OpenVPN.Configuration.self, from: data) else { - DispatchQueue.main.async { - completionHandler(nil) - } - return - } - DispatchQueue.main.async { - completionHandler(cfg) - } - } - } catch { - DispatchQueue.main.async { - completionHandler(nil) - } - } - } - } - - // MARK: Helpers - - private func findAndRequestDebugLog(completionHandler: @escaping (String?) -> Void) { - 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(Message.requestLog.data) { (data) in - guard let data = data, !data.isEmpty else { - completionHandler(nil) - return - } - let newestLog = String(data: data, encoding: .utf8) - completionHandler(newestLog) - } - } catch { - completionHandler(nil) - } - } -} diff --git a/Sources/TunnelKitOpenVPNProtocol/ControlChannel.swift b/Sources/TunnelKitOpenVPNProtocol/ControlChannel.swift index 615f3dc..5d1c040 100644 --- a/Sources/TunnelKitOpenVPNProtocol/ControlChannel.swift +++ b/Sources/TunnelKitOpenVPNProtocol/ControlChannel.swift @@ -234,8 +234,8 @@ extension OpenVPN { dataCount.outbound += count } - func currentDataCount() -> (Int, Int) { - return dataCount.pair + func currentDataCount() -> DataCount { + return DataCount(UInt(dataCount.inbound), UInt(dataCount.outbound)) } } } diff --git a/Sources/TunnelKitOpenVPNProtocol/OpenVPNSession.swift b/Sources/TunnelKitOpenVPNProtocol/OpenVPNSession.swift index 31dde84..e7b92db 100644 --- a/Sources/TunnelKitOpenVPNProtocol/OpenVPNSession.swift +++ b/Sources/TunnelKitOpenVPNProtocol/OpenVPNSession.swift @@ -276,7 +276,7 @@ public class OpenVPNSession: Session { loopTunnel() } - public func dataCount() -> (Int, Int)? { + public func dataCount() -> DataCount? { guard let _ = link else { return nil } diff --git a/Sources/TunnelKitWireGuardAppExtension/Internal/ErrorNotifier.swift b/Sources/TunnelKitWireGuardAppExtension/Internal/ErrorNotifier.swift deleted file mode 100644 index 37b7e7f..0000000 --- a/Sources/TunnelKitWireGuardAppExtension/Internal/ErrorNotifier.swift +++ /dev/null @@ -1,42 +0,0 @@ -import TunnelKitWireGuardCore -import TunnelKitWireGuardManager - -// SPDX-License-Identifier: MIT -// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved. - -import NetworkExtension - -class ErrorNotifier { - private let appGroupId: String - - private var sharedFolderURL: URL? { - guard let sharedFolderURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else { - wg_log(.error, message: "Cannot obtain shared folder URL") - return nil - } - return sharedFolderURL - } - - init(appGroupId: String) { - self.appGroupId = appGroupId - removeLastErrorFile() - } - - func notify(_ error: WireGuardProviderError) { - guard let lastErrorFilePath = networkExtensionLastErrorFileURL?.path else { - return - } - let errorMessageData = "\(error)".data(using: .utf8) - FileManager.default.createFile(atPath: lastErrorFilePath, contents: errorMessageData, attributes: nil) - } - - func removeLastErrorFile() { - if let lastErrorFileURL = networkExtensionLastErrorFileURL { - try? FileManager.default.removeItem(at: lastErrorFileURL) - } - } - - private var networkExtensionLastErrorFileURL: URL? { - return sharedFolderURL?.appendingPathComponent("last-error.txt") - } -} diff --git a/Sources/TunnelKitWireGuardAppExtension/WireGuardTunnelProvider.swift b/Sources/TunnelKitWireGuardAppExtension/WireGuardTunnelProvider.swift index 3752b0c..5820d27 100644 --- a/Sources/TunnelKitWireGuardAppExtension/WireGuardTunnelProvider.swift +++ b/Sources/TunnelKitWireGuardAppExtension/WireGuardTunnelProvider.swift @@ -1,6 +1,8 @@ import TunnelKitWireGuardCore import TunnelKitWireGuardManager import WireGuardKit +import __TunnelKitUtils +import SwiftyBeaver // SPDX-License-Identifier: MIT // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved. @@ -10,7 +12,7 @@ import NetworkExtension import os open class WireGuardTunnelProvider: NEPacketTunnelProvider { - private var persistentErrorNotifier: ErrorNotifier? + private var cfg: WireGuard.ProviderConfiguration! private lazy var adapter: WireGuardAdapter = { return WireGuardAdapter(with: self) { logLevel, message in @@ -25,20 +27,20 @@ open class WireGuardTunnelProvider: NEPacketTunnelProvider { guard let tunnelProviderProtocol = protocolConfiguration as? NETunnelProviderProtocol else { fatalError("Not a NETunnelProviderProtocol") } - guard let appGroup = tunnelProviderProtocol.providerConfiguration?["AppGroup"] as? String else { - fatalError("AppGroup not found in providerConfiguration") + guard let providerConfiguration = tunnelProviderProtocol.providerConfiguration else { + fatalError("Missing providerConfiguration") } - let errorNotifier = ErrorNotifier(appGroupId: appGroup) - persistentErrorNotifier = errorNotifier - + let tunnelConfiguration: TunnelConfiguration do { - tunnelConfiguration = try WireGuardProvider.Configuration.parsed(from: tunnelProviderProtocol).tunnelConfiguration + cfg = try fromDictionary(WireGuard.ProviderConfiguration.self, providerConfiguration) + tunnelConfiguration = cfg.configuration.tunnelConfiguration } catch { - errorNotifier.notify(WireGuardProviderError.savedProtocolConfigurationIsInvalid) completionHandler(WireGuardProviderError.savedProtocolConfigurationIsInvalid) return } + + configureLogging(debug: cfg.shouldDebug) // END: TunnelKit @@ -56,24 +58,24 @@ open class WireGuardTunnelProvider: NEPacketTunnelProvider { switch adapterError { case .cannotLocateTunnelFileDescriptor: wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor") - errorNotifier.notify(WireGuardProviderError.couldNotDetermineFileDescriptor) + self.cfg.lastError = .couldNotDetermineFileDescriptor completionHandler(WireGuardProviderError.couldNotDetermineFileDescriptor) case .dnsResolution(let dnsErrors): let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address } .joined(separator: ", ") wg_log(.error, message: "DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)") - errorNotifier.notify(WireGuardProviderError.dnsResolutionFailure) + self.cfg.lastError = .dnsResolutionFailure completionHandler(WireGuardProviderError.dnsResolutionFailure) case .setNetworkSettings(let error): wg_log(.error, message: "Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)") - errorNotifier.notify(WireGuardProviderError.couldNotSetNetworkSettings) + self.cfg.lastError = .couldNotSetNetworkSettings completionHandler(WireGuardProviderError.couldNotSetNetworkSettings) case .startWireGuardBackend(let errorCode): wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)") - errorNotifier.notify(WireGuardProviderError.couldNotStartBackend) + self.cfg.lastError = .couldNotStartBackend completionHandler(WireGuardProviderError.couldNotStartBackend) case .invalidState: @@ -88,7 +90,7 @@ open class WireGuardTunnelProvider: NEPacketTunnelProvider { adapter.stop { error in // BEGIN: TunnelKit - self.persistentErrorNotifier?.removeLastErrorFile() + self.cfg.lastError = nil // END: TunnelKit if let error = error { @@ -122,6 +124,27 @@ open class WireGuardTunnelProvider: NEPacketTunnelProvider { } } +extension WireGuardTunnelProvider { + private func configureLogging(debug: Bool, customFormat: String? = nil) { + let logLevel: SwiftyBeaver.Level = (debug ? .debug : .info) + let logFormat = customFormat ?? "$Dyyyy-MM-dd HH:mm:ss.SSS$d $L $N.$F:$l - $M" + + if debug { + let console = ConsoleDestination() + console.useNSLog = true + console.minLevel = logLevel + console.format = logFormat + SwiftyBeaver.addDestination(console) + } + + let file = FileDestination(logFileURL: cfg.urlForDebugLog) + file.minLevel = logLevel + file.format = logFormat + file.logFileMaxSize = 20000 + SwiftyBeaver.addDestination(file) + } +} + extension WireGuardLogLevel { var osLogLevel: OSLogType { switch self { diff --git a/Sources/TunnelKitWireGuardCore/Internal/Logger.swift b/Sources/TunnelKitWireGuardCore/Internal/Logger.swift index 8676835..b02ca7f 100644 --- a/Sources/TunnelKitWireGuardCore/Internal/Logger.swift +++ b/Sources/TunnelKitWireGuardCore/Internal/Logger.swift @@ -1,3 +1,25 @@ +import SwiftyBeaver + +private let log = SwiftyBeaver.self + +extension OSLogType { + var sbLevel: SwiftyBeaver.Level { + switch self { + case .debug: + return .debug + + case .info: + return .info + + case .error, .fault: + return .error + + default: + return .info + } + } +} + // SPDX-License-Identifier: MIT // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved. @@ -6,8 +28,10 @@ import os.log public func wg_log(_ type: OSLogType, staticMessage msg: StaticString) { os_log(msg, log: OSLog.default, type: type) + log.custom(level: type.sbLevel, message: msg, context: nil) } public func wg_log(_ type: OSLogType, message msg: String) { os_log("%{public}s", log: OSLog.default, type: type, msg) + log.custom(level: type.sbLevel, message: msg, context: nil) } diff --git a/Sources/TunnelKitWireGuardManager/WireGuard+ProviderConfiguration.swift b/Sources/TunnelKitWireGuardManager/WireGuard+ProviderConfiguration.swift new file mode 100644 index 0000000..ec51b8c --- /dev/null +++ b/Sources/TunnelKitWireGuardManager/WireGuard+ProviderConfiguration.swift @@ -0,0 +1,155 @@ +// +// WireGuard+ProviderConfiguration.swift +// TunnelKit +// +// Created by Davide De Rosa on 11/21/21. +// Copyright (c) 2022 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 TunnelKitManager +import TunnelKitWireGuardCore +import WireGuardKit +import __TunnelKitUtils +import SwiftyBeaver + +private let log = SwiftyBeaver.self + +extension WireGuard { + + /// Specific configuration for WireGuard. + public struct ProviderConfiguration: Codable { + fileprivate enum Filenames: String { + case debugLog = "WireGuard.Tunnel.log" + } + + fileprivate enum Keys: String { + case lastError = "WireGuard.LastError" + } + + public let title: String + + public let appGroup: String + + public let configuration: WireGuard.Configuration + + public var shouldDebug = false + + public init(_ title: String, appGroup: String, configuration: WireGuard.Configuration) { + self.title = title + self.appGroup = appGroup + self.configuration = configuration + } + + private init(_ title: String, appGroup: String, wgQuickConfig: String) throws { + self.title = title + self.appGroup = appGroup + configuration = try WireGuard.Configuration(wgQuickConfig: wgQuickConfig) + } + } +} + +// MARK: NetworkExtensionConfiguration + +extension WireGuard.ProviderConfiguration: NetworkExtensionConfiguration { + + /// :nodoc: + public func asTunnelProtocol( + withBundleIdentifier tunnelBundleIdentifier: String, + extra: NetworkExtensionExtra? + ) throws -> NETunnelProviderProtocol { + let protocolConfiguration = NETunnelProviderProtocol() + protocolConfiguration.providerBundleIdentifier = tunnelBundleIdentifier + protocolConfiguration.serverAddress = configuration.endpointRepresentation + protocolConfiguration.passwordReference = extra?.passwordReference + protocolConfiguration.disconnectOnSleep = extra?.disconnectsOnSleep ?? false + protocolConfiguration.providerConfiguration = try asDictionary() + return protocolConfiguration + } +} + +// MARK: Shared data + +extension WireGuard.ProviderConfiguration { + public var lastError: WireGuardProviderError? { + get { + return defaults?.wireGuardLastError + } + set { + defaults?.wireGuardLastError = newValue + } + } + + private var defaults: UserDefaults? { + return UserDefaults(suiteName: appGroup) + } + + public var urlForDebugLog: URL? { + return FileManager.default.wireGuardURLForDebugLog(appGroup: appGroup) + } + + public var debugLog: String? { + return FileManager.default.wireGuardDebugLog(appGroup: appGroup) + } +} + +/// :nodoc: +extension UserDefaults { + public var wireGuardLastError: WireGuardProviderError? { + get { + guard let rawValue = string(forKey: WireGuard.ProviderConfiguration.Keys.lastError.rawValue) else { + return nil + } + return WireGuardProviderError(rawValue: rawValue) + } + set { + guard let newValue = newValue else { + removeObject(forKey: WireGuard.ProviderConfiguration.Keys.lastError.rawValue) + return + } + set(newValue.rawValue, forKey: WireGuard.ProviderConfiguration.Keys.lastError.rawValue) + } + } +} + +/// :nodoc: +extension FileManager { + public func wireGuardURLForDebugLog(appGroup: String) -> URL? { + return documentsURL(appGroup: appGroup)? + .appendingPathComponent(WireGuard.ProviderConfiguration.Filenames.debugLog.rawValue) + } + + public func wireGuardDebugLog(appGroup: String) -> String? { + guard let url = wireGuardURLForDebugLog(appGroup: appGroup) else { + return nil + } + do { + return try String(contentsOf: url) + } catch { + log.error("Unable to access debug log: \(error)") + return nil + } + } + + private func documentsURL(appGroup: String) -> URL? { + return containerURL(forSecurityApplicationGroupIdentifier: appGroup) + } +} diff --git a/Sources/TunnelKitWireGuardManager/WireGuardProvider+Configuration.swift b/Sources/TunnelKitWireGuardManager/WireGuardProvider+Configuration.swift deleted file mode 100644 index 8e7cdd1..0000000 --- a/Sources/TunnelKitWireGuardManager/WireGuardProvider+Configuration.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// WireGuardProvider+Configuration.swift -// TunnelKit -// -// Created by Davide De Rosa on 11/21/21. -// Copyright (c) 2022 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 TunnelKitManager -import TunnelKitWireGuardCore -import WireGuardKit - -extension WireGuardProvider { - public struct Configuration { - public let innerConfiguration: WireGuard.Configuration - - // required by WireGuardTunnelProvider - public var tunnelConfiguration: TunnelConfiguration { - return innerConfiguration.tunnelConfiguration - } - - public init(innerConfiguration: WireGuard.Configuration) { - self.innerConfiguration = innerConfiguration - } - - private init(wgQuickConfig: String) throws { - innerConfiguration = try WireGuard.Configuration(wgQuickConfig: wgQuickConfig) - } - - public func generatedTunnelProtocol(withBundleIdentifier bundleIdentifier: String, appGroup: String, context: String) throws -> NETunnelProviderProtocol { - - let protocolConfiguration = NETunnelProviderProtocol() - protocolConfiguration.providerBundleIdentifier = bundleIdentifier - protocolConfiguration.serverAddress = innerConfiguration.endpointRepresentation - - let keychain = Keychain(group: appGroup) - let wgString = innerConfiguration.asWgQuickConfig() - protocolConfiguration.passwordReference = try keychain.set(password: wgString, for: "", context: context) - protocolConfiguration.providerConfiguration = ["AppGroup": appGroup] - - return protocolConfiguration - } - - public static func appGroup(from protocolConfiguration: NETunnelProviderProtocol) throws -> String { - guard let appGroup = protocolConfiguration.providerConfiguration?["AppGroup"] as? String else { - throw WireGuardProviderError.savedProtocolConfigurationIsInvalid - } - return appGroup - } - - public static func parsed(from protocolConfiguration: NETunnelProviderProtocol) throws -> Configuration { - guard let passwordReference = protocolConfiguration.passwordReference, - let wgString = try? Keychain.password(forReference: passwordReference) else { - - throw WireGuardProviderError.savedProtocolConfigurationIsInvalid - } - return try Configuration(wgQuickConfig: wgString) - } - } -} diff --git a/Sources/TunnelKitWireGuardManager/WireGuardProvider.swift b/Sources/TunnelKitWireGuardManager/WireGuardProvider.swift deleted file mode 100644 index 122dbc8..0000000 --- a/Sources/TunnelKitWireGuardManager/WireGuardProvider.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// WireGuardProvider.swift -// TunnelKit -// -// Created by Davide De Rosa on 11/21/21. -// Copyright (c) 2022 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 TunnelKitManager - -public class WireGuardProvider: VPNProvider { - private let provider: NetworkExtensionVPNProvider - - public init(bundleIdentifier: String) { - provider = NetworkExtensionVPNProvider(locator: NetworkExtensionTunnelLocator(bundleIdentifier: bundleIdentifier)) - } - - // 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, delay: Double? = nil, completionHandler: ((Error?) -> Void)?) { - provider.reconnect(configuration: configuration, delay: delay, completionHandler: completionHandler) - } - - public func uninstall(completionHandler: (() -> Void)?) { - provider.uninstall(completionHandler: completionHandler) - } -} diff --git a/Tests/TunnelKitOpenVPNTests/AppExtensionTests.swift b/Tests/TunnelKitOpenVPNTests/AppExtensionTests.swift index a61d8b7..a3c6c46 100644 --- a/Tests/TunnelKitOpenVPNTests/AppExtensionTests.swift +++ b/Tests/TunnelKitOpenVPNTests/AppExtensionTests.swift @@ -56,54 +56,48 @@ class AppExtensionTests: XCTestCase { } func testConfiguration() { - var builder: OpenVPNProvider.ConfigurationBuilder! - var cfg: OpenVPNProvider.Configuration! - - let identifier = "com.example.Provider" + let bundleIdentifier = "com.example.Provider" let appGroup = "group.com.algoritmico.TunnelKit" + let hostname = "example.com" let port: UInt16 = 1234 let serverAddress = "\(hostname):\(port)" - let context = "foobar" let credentials = OpenVPN.Credentials("foo", "bar") - var sessionBuilder = OpenVPN.ConfigurationBuilder() - sessionBuilder.ca = OpenVPN.CryptoContainer(pem: "abcdef") - sessionBuilder.cipher = .aes128cbc - sessionBuilder.digest = .sha256 - sessionBuilder.remotes = [.init(hostname, .init(.udp, port))] - sessionBuilder.mtu = 1230 - builder = OpenVPNProvider.ConfigurationBuilder(sessionConfiguration: sessionBuilder.build()) - XCTAssertNotNil(builder) + var builder = OpenVPN.ConfigurationBuilder() + builder.ca = OpenVPN.CryptoContainer(pem: "abcdef") + builder.cipher = .aes128cbc + builder.digest = .sha256 + builder.remotes = [.init(hostname, .init(.udp, port))] + builder.mtu = 1230 - cfg = builder.build() - - let proto = try? cfg.generatedTunnelProtocol( - withBundleIdentifier: identifier, - appGroup: appGroup, - context: context, - credentials: credentials - ) - XCTAssertNotNil(proto) + var cfg = OpenVPN.ProviderConfiguration("", appGroup: appGroup, configuration: builder.build()) + cfg.username = credentials.username + let proto: NETunnelProviderProtocol + do { + proto = try cfg.asTunnelProtocol(withBundleIdentifier: bundleIdentifier, extra: nil) + } catch { + XCTFail(error.localizedDescription) + return + } - XCTAssertEqual(proto?.providerBundleIdentifier, identifier) - XCTAssertEqual(proto?.serverAddress, serverAddress) - XCTAssertEqual(proto?.username, credentials.username) - XCTAssertEqual(proto?.passwordReference, try? Keychain(group: appGroup).passwordReference(for: credentials.username, context: context)) + XCTAssertEqual(proto.providerBundleIdentifier, bundleIdentifier) + XCTAssertEqual(proto.serverAddress, serverAddress) + XCTAssertEqual(proto.username, credentials.username) - guard let pc = proto?.providerConfiguration else { + guard let pc = proto.providerConfiguration else { return } print("\(pc)") - let pcSession = pc["sessionConfiguration"] as? [String: Any] + let ovpn = pc["configuration"] as? [String: Any] XCTAssertEqual(pc["appGroup"] as? String, appGroup) XCTAssertEqual(pc["shouldDebug"] as? Bool, cfg.shouldDebug) - XCTAssertEqual(pcSession?["cipher"] as? String, cfg.sessionConfiguration.cipher?.rawValue) - XCTAssertEqual(pcSession?["digest"] as? String, cfg.sessionConfiguration.digest?.rawValue) - XCTAssertEqual(pcSession?["ca"] as? String, cfg.sessionConfiguration.ca?.pem) - XCTAssertEqual(pcSession?["mtu"] as? Int, cfg.sessionConfiguration.mtu) - XCTAssertEqual(pcSession?["renegotiatesAfter"] as? TimeInterval, cfg.sessionConfiguration.renegotiatesAfter) + XCTAssertEqual(ovpn?["cipher"] as? String, cfg.configuration.cipher?.rawValue) + XCTAssertEqual(ovpn?["digest"] as? String, cfg.configuration.digest?.rawValue) + XCTAssertEqual(ovpn?["ca"] as? String, cfg.configuration.ca?.pem) + XCTAssertEqual(ovpn?["mtu"] as? Int, cfg.configuration.mtu) + XCTAssertEqual(ovpn?["renegotiatesAfter"] as? TimeInterval, cfg.configuration.renegotiatesAfter) } func testDNSResolver() {