tunnelkit/Sources/TunnelKitManager/NetworkExtensionVPN.swift
Davide De Rosa 3741a17c20 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.
2022-03-12 10:35:39 +01:00

296 lines
9.4 KiB
Swift

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