Rewrite Manager package to make it stateless

In order to avoid chaos from multiple profiles, retain the
profile to be installed and remove all the other ones. Also,
make sure to do the removal AFTER install, as doing it
before would trigger the VPN permission alert again.

XXX: there is some weird behavior from NetworkExtension
occasionally sending notifications with a bogus NEVPNManager
object having a nil .localizedDescription and other properties set
to nonsensical values. Discard the notification when such an object
is identified.

Encapsulate extra NetworkExtension settings:

- passwordReference
- onDemandRules
- disconnectsOnSleep

Also:

- Only set on-demand if any rules are set
- Assume VPN is enabled even with on-demand disabled
- Use DataCount instead of raw Int pair

Attach useful information to VPN notifications:

- VPN isEnabled
- VPN status
- VPN command error
- Tunnel bundle identifier (if available)

Expose specific OpenVPN/WireGuard shared data via extensions in
UserDefaults/FileManager.

Finally, drop incomplete IKE support. No fit.
This commit is contained in:
Davide De Rosa 2022-03-05 00:58:07 +01:00
parent 133b4b2337
commit 3741a17c20
38 changed files with 1376 additions and 1701 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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, Error>) -> 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)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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<NETunnelProviderManager, Error>) -> Void)?
) {
let proto: NETunnelProviderProtocol
do {
proto = try configuration.asTunnelProtocol(
withBundleIdentifier: tunnelBundleIdentifier,
extra: extra
)
} catch {
completionHandler?(.failure(error))
return
}
lookupAll { result in
switch result {
case .success(let managers):
// install (new or existing) then callback
let targetManager = managers.first {
$0.isTunnel(withIdentifier: tunnelBundleIdentifier)
} ?? NETunnelProviderManager()
self.install(
targetManager,
title: configuration.title,
protocolConfiguration: proto,
onDemandRules: extra?.onDemandRules ?? [],
completionHandler: completionHandler
)
// remove others afterwards (to avoid permission request)
managers.filter {
!$0.isTunnel(withIdentifier: tunnelBundleIdentifier)
}.forEach {
$0.removeFromPreferences(completionHandler: nil)
}
case .failure(let error):
completionHandler?(.failure(error))
self.notifyError(error)
}
}
}
public func reconnect(
_ tunnelBundleIdentifier: String,
configuration: NetworkExtensionConfiguration,
extra: Extra?,
delay: Double?
) {
let delay = delay ?? 2.0
install(
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<NETunnelProviderManager, Error>) -> 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
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)?)
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}

View File

@ -25,7 +25,7 @@
import Foundation
/// Status of a `VPNProvider`.
/// Status of a `VPN`.
public enum VPNStatus: String {
/// VPN is connected.

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
// 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)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
// 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)
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}
}

View File

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

View File

@ -276,7 +276,7 @@ public class OpenVPNSession: Session {
loopTunnel()
}
public func dataCount() -> (Int, Int)? {
public func dataCount() -> DataCount? {
guard let _ = link else {
return nil
}

View File

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

View File

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

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}

View File

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