mirror of
https://github.com/passepartoutvpn/wireguard-apple.git
synced 2025-02-14 20:02:02 +00:00
488 lines
22 KiB
Swift
488 lines
22 KiB
Swift
// SPDX-License-Identifier: MIT
|
|
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
|
|
|
import Foundation
|
|
import NetworkExtension
|
|
import os.log
|
|
|
|
protocol TunnelsManagerListDelegate: class {
|
|
func tunnelAdded(at index: Int)
|
|
func tunnelModified(at index: Int)
|
|
func tunnelMoved(from oldIndex: Int, to newIndex: Int)
|
|
func tunnelRemoved(at index: Int)
|
|
}
|
|
|
|
protocol TunnelsManagerActivationDelegate: class {
|
|
func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) // startTunnel wasn't called or failed
|
|
func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) // startTunnel succeeded
|
|
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) // status didn't change to connected
|
|
func tunnelActivationSucceeded(tunnel: TunnelContainer) // status changed to connected
|
|
}
|
|
|
|
enum TunnelsManagerActivationAttemptError: WireGuardAppError {
|
|
case tunnelIsNotInactive
|
|
case anotherTunnelIsOperational(otherTunnelName: String)
|
|
case failedWhileStarting // startTunnel() throwed
|
|
case failedWhileSaving // save config after re-enabling throwed
|
|
case failedWhileLoading // reloading config throwed
|
|
case failedBecauseOfTooManyErrors // recursion limit reached
|
|
|
|
var alertText: AlertText {
|
|
switch self {
|
|
case .tunnelIsNotInactive:
|
|
return ("Activation failure", "The tunnel is already active or in the process of being activated")
|
|
case .anotherTunnelIsOperational(let otherTunnelName):
|
|
return ("Activation failure", "Please disconnect '\(otherTunnelName)' before enabling this tunnel.")
|
|
case .failedWhileStarting, .failedWhileSaving, .failedWhileLoading, .failedBecauseOfTooManyErrors:
|
|
return ("Activation failure", "The tunnel could not be activated.")
|
|
}
|
|
}
|
|
}
|
|
|
|
enum TunnelsManagerActivationError: WireGuardAppError {
|
|
case activationFailed
|
|
case activationFailedWithExtensionError(title: String, message: String)
|
|
var alertText: AlertText {
|
|
switch self {
|
|
case .activationFailed:
|
|
return ("Activation failure", "The tunnel could not be activated. Please ensure that you are connected to the Internet.")
|
|
case .activationFailedWithExtensionError(let title, let message):
|
|
return (title, message)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum TunnelsManagerError: WireGuardAppError {
|
|
case tunnelNameEmpty
|
|
case tunnelAlreadyExistsWithThatName
|
|
case systemErrorOnListingTunnels
|
|
case systemErrorOnAddTunnel
|
|
case systemErrorOnModifyTunnel
|
|
case systemErrorOnRemoveTunnel
|
|
|
|
var alertText: AlertText {
|
|
switch self {
|
|
case .tunnelNameEmpty:
|
|
return ("No name provided", "Cannot create tunnel with an empty name")
|
|
case .tunnelAlreadyExistsWithThatName:
|
|
return ("Name already exists", "A tunnel with that name already exists")
|
|
case .systemErrorOnListingTunnels:
|
|
return ("Unable to list tunnels", "")
|
|
case .systemErrorOnAddTunnel:
|
|
return ("Unable to create tunnel", "")
|
|
case .systemErrorOnModifyTunnel:
|
|
return ("Unable to modify tunnel", "")
|
|
case .systemErrorOnRemoveTunnel:
|
|
return ("Unable to remove tunnel", "")
|
|
}
|
|
}
|
|
}
|
|
|
|
class TunnelsManager {
|
|
private var tunnels: [TunnelContainer]
|
|
weak var tunnelsListDelegate: TunnelsManagerListDelegate?
|
|
weak var activationDelegate: TunnelsManagerActivationDelegate?
|
|
private var statusObservationToken: AnyObject?
|
|
|
|
init(tunnelProviders: [NETunnelProviderManager]) {
|
|
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
|
|
startObservingTunnelStatuses()
|
|
}
|
|
|
|
static func create(completionHandler: @escaping (WireGuardResult<TunnelsManager>) -> Void) {
|
|
#if targetEnvironment(simulator)
|
|
completionHandler(.success(TunnelsManager(tunnelProviders: MockTunnels.createMockTunnels())))
|
|
#else
|
|
NETunnelProviderManager.loadAllFromPreferences { managers, error in
|
|
if let error = error {
|
|
wg_log(.error, message: "Failed to load tunnel provider managers: \(error)")
|
|
completionHandler(.failure(TunnelsManagerError.systemErrorOnListingTunnels))
|
|
return
|
|
}
|
|
completionHandler(.success(TunnelsManager(tunnelProviders: managers ?? [])))
|
|
}
|
|
#endif
|
|
}
|
|
|
|
func add(tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting, completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
|
|
let tunnelName = tunnelConfiguration.interface.name
|
|
if tunnelName.isEmpty {
|
|
completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
|
|
return
|
|
}
|
|
|
|
if tunnels.contains(where: { $0.name == tunnelName }) {
|
|
completionHandler(.failure(TunnelsManagerError.tunnelAlreadyExistsWithThatName))
|
|
return
|
|
}
|
|
|
|
let tunnelProviderManager = NETunnelProviderManager()
|
|
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
|
|
tunnelProviderManager.localizedDescription = tunnelName
|
|
tunnelProviderManager.isEnabled = true
|
|
|
|
activateOnDemandSetting.apply(on: tunnelProviderManager)
|
|
|
|
tunnelProviderManager.saveToPreferences { [weak self] error in
|
|
guard error == nil else {
|
|
wg_log(.error, message: "Add: Saving configuration failed: \(error!)")
|
|
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel))
|
|
return
|
|
}
|
|
|
|
guard let self = self else { return }
|
|
|
|
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
|
|
self.tunnels.append(tunnel)
|
|
self.tunnels.sort { $0.name < $1.name }
|
|
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
|
|
completionHandler(.success(tunnel))
|
|
}
|
|
}
|
|
|
|
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt) -> Void) {
|
|
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, completionHandler: completionHandler)
|
|
}
|
|
|
|
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, completionHandler: @escaping (UInt) -> Void) {
|
|
guard let head = tunnelConfigurations.first else {
|
|
completionHandler(numberSuccessful)
|
|
return
|
|
}
|
|
let tail = tunnelConfigurations.dropFirst()
|
|
add(tunnelConfiguration: head) { [weak self, tail] result in
|
|
DispatchQueue.main.async {
|
|
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (result.isSuccess ? 1 : 0), completionHandler: completionHandler)
|
|
}
|
|
}
|
|
}
|
|
|
|
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
|
let tunnelName = tunnelConfiguration.interface.name
|
|
if tunnelName.isEmpty {
|
|
completionHandler(TunnelsManagerError.tunnelNameEmpty)
|
|
return
|
|
}
|
|
|
|
let tunnelProviderManager = tunnel.tunnelProvider
|
|
let isNameChanged = tunnelName != tunnelProviderManager.localizedDescription
|
|
if isNameChanged {
|
|
guard !tunnels.contains(where: { $0.name == tunnelName }) else {
|
|
completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
|
|
return
|
|
}
|
|
tunnel.name = tunnelName
|
|
}
|
|
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
|
|
tunnelProviderManager.localizedDescription = tunnelName
|
|
tunnelProviderManager.isEnabled = true
|
|
|
|
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && activateOnDemandSetting.isActivateOnDemandEnabled
|
|
activateOnDemandSetting.apply(on: tunnelProviderManager)
|
|
|
|
tunnelProviderManager.saveToPreferences { [weak self] error in
|
|
guard error == nil else {
|
|
wg_log(.error, message: "Modify: Saving configuration failed: \(error!)")
|
|
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel)
|
|
return
|
|
}
|
|
guard let self = self else { return }
|
|
|
|
if isNameChanged {
|
|
let oldIndex = self.tunnels.firstIndex(of: tunnel)!
|
|
self.tunnels.sort { $0.name < $1.name }
|
|
let newIndex = self.tunnels.firstIndex(of: tunnel)!
|
|
self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex)
|
|
}
|
|
self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!)
|
|
|
|
if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting {
|
|
// Turn off the tunnel, and then turn it back on, so the changes are made effective
|
|
tunnel.status = .restarting
|
|
(tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
|
|
}
|
|
|
|
if isActivatingOnDemand {
|
|
// Reload tunnel after saving.
|
|
// Without this, the tunnel stopes getting updates on the tunnel status from iOS.
|
|
tunnelProviderManager.loadFromPreferences { error in
|
|
tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
|
|
guard error == nil else {
|
|
wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)")
|
|
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel)
|
|
return
|
|
}
|
|
completionHandler(nil)
|
|
}
|
|
} else {
|
|
completionHandler(nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
|
let tunnelProviderManager = tunnel.tunnelProvider
|
|
|
|
tunnelProviderManager.removeFromPreferences { [weak self] error in
|
|
guard error == nil else {
|
|
wg_log(.error, message: "Remove: Saving configuration failed: \(error!)")
|
|
completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel)
|
|
return
|
|
}
|
|
if let self = self {
|
|
let index = self.tunnels.firstIndex(of: tunnel)!
|
|
self.tunnels.remove(at: index)
|
|
self.tunnelsListDelegate?.tunnelRemoved(at: index)
|
|
}
|
|
completionHandler(nil)
|
|
}
|
|
}
|
|
|
|
func numberOfTunnels() -> Int {
|
|
return tunnels.count
|
|
}
|
|
|
|
func tunnel(at index: Int) -> TunnelContainer {
|
|
return tunnels[index]
|
|
}
|
|
|
|
func tunnel(named tunnelName: String) -> TunnelContainer? {
|
|
return tunnels.first { $0.name == tunnelName }
|
|
}
|
|
|
|
func startActivation(of tunnel: TunnelContainer) {
|
|
guard tunnels.contains(tunnel) else { return } // Ensure it's not deleted
|
|
guard tunnel.status == .inactive else {
|
|
activationDelegate?.tunnelActivationAttemptFailed(tunnel: tunnel, error: .tunnelIsNotInactive)
|
|
return
|
|
}
|
|
|
|
if let alreadyWaitingTunnel = tunnels.first(where: { $0.status == .waiting }) {
|
|
alreadyWaitingTunnel.status = .inactive
|
|
}
|
|
|
|
if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) {
|
|
wg_log(.info, message: "Tunnel '\(tunnel.name)' waiting for deactivation of '\(tunnelInOperation.name)'")
|
|
tunnel.status = .waiting
|
|
if tunnelInOperation.status != .deactivating {
|
|
startDeactivation(of: tunnelInOperation)
|
|
}
|
|
return
|
|
}
|
|
|
|
#if targetEnvironment(simulator)
|
|
tunnel.status = .active
|
|
#else
|
|
tunnel.startActivation(activationDelegate: activationDelegate)
|
|
#endif
|
|
}
|
|
|
|
func startDeactivation(of tunnel: TunnelContainer) {
|
|
tunnel.isAttemptingActivation = false
|
|
guard tunnel.status != .inactive && tunnel.status != .deactivating else { return }
|
|
#if targetEnvironment(simulator)
|
|
tunnel.status = .inactive
|
|
#else
|
|
tunnel.startDeactivation()
|
|
#endif
|
|
}
|
|
|
|
func refreshStatuses() {
|
|
tunnels.forEach { $0.refreshStatus() }
|
|
}
|
|
|
|
private func startObservingTunnelStatuses() {
|
|
guard statusObservationToken == nil else { return }
|
|
|
|
statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
|
|
guard let self = self,
|
|
let session = statusChangeNotification.object as? NETunnelProviderSession,
|
|
let tunnelProvider = session.manager as? NETunnelProviderManager,
|
|
let tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
|
|
|
|
wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'")
|
|
|
|
// Track what happened to our attempt to start the tunnel
|
|
if tunnel.isAttemptingActivation {
|
|
if session.status == .connected {
|
|
tunnel.isAttemptingActivation = false
|
|
self.activationDelegate?.tunnelActivationSucceeded(tunnel: tunnel)
|
|
} else if session.status == .disconnected {
|
|
tunnel.isAttemptingActivation = false
|
|
if let (title, message) = self.lastErrorTextFromNetworkExtension(for: tunnel) {
|
|
self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailedWithExtensionError(title: title, message: message))
|
|
} else {
|
|
self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailed)
|
|
}
|
|
}
|
|
}
|
|
|
|
// In case we're restarting the tunnel
|
|
if (tunnel.status == .restarting) && (session.status == .disconnected || session.status == .disconnecting) {
|
|
// Don't change tunnel.status when disconnecting for a restart
|
|
if session.status == .disconnected {
|
|
tunnel.startActivation(activationDelegate: self.activationDelegate)
|
|
}
|
|
return
|
|
}
|
|
|
|
tunnel.refreshStatus()
|
|
|
|
// In case some other tunnel is waiting for this tunnel to get deactivated
|
|
if session.status == .disconnected || session.status == .invalid {
|
|
if let waitingTunnel = self.tunnels.first(where: { $0.status == .waiting }) {
|
|
waitingTunnel.startActivation(activationDelegate: self.activationDelegate)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func lastErrorTextFromNetworkExtension(for tunnel: TunnelContainer) -> (title: String, message: String)? {
|
|
guard let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL else { return nil }
|
|
guard let lastErrorData = try? Data(contentsOf: lastErrorFileURL) else { return nil }
|
|
guard let lastErrorText = String(data: lastErrorData, encoding: .utf8) else { return nil }
|
|
let lastErrorStrings = lastErrorText.split(separator: "\n").map { String($0) }
|
|
guard lastErrorStrings.count == 3 else { return nil }
|
|
let attemptIdInDisk = lastErrorStrings[0]
|
|
if let attemptIdForTunnel = tunnel.activationAttemptId, attemptIdInDisk == attemptIdForTunnel {
|
|
return (title: lastErrorStrings[1], message: lastErrorStrings[2])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
deinit {
|
|
if let statusObservationToken = statusObservationToken {
|
|
NotificationCenter.default.removeObserver(statusObservationToken)
|
|
}
|
|
}
|
|
}
|
|
|
|
class TunnelContainer: NSObject {
|
|
@objc dynamic var name: String
|
|
@objc dynamic var status: TunnelStatus
|
|
|
|
@objc dynamic var isActivateOnDemandEnabled: Bool
|
|
|
|
var isAttemptingActivation = false {
|
|
didSet {
|
|
if isAttemptingActivation {
|
|
let activationTimer = Timer(timeInterval: 5 /* seconds */, repeats: true) { [weak self] _ in
|
|
guard let self = self else { return }
|
|
self.refreshStatus()
|
|
if self.status == .inactive || self.status == .active {
|
|
self.isAttemptingActivation = false // This also invalidates the timer
|
|
}
|
|
}
|
|
self.activationTimer = activationTimer
|
|
RunLoop.main.add(activationTimer, forMode: .default)
|
|
} else {
|
|
activationTimer?.invalidate()
|
|
activationTimer = nil
|
|
}
|
|
}
|
|
}
|
|
var activationAttemptId: String?
|
|
var activationTimer: Timer?
|
|
|
|
fileprivate let tunnelProvider: NETunnelProviderManager
|
|
private var lastTunnelConnectionStatus: NEVPNStatus?
|
|
|
|
init(tunnel: NETunnelProviderManager) {
|
|
name = tunnel.localizedDescription ?? "Unnamed"
|
|
let status = TunnelStatus(from: tunnel.connection.status)
|
|
self.status = status
|
|
isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
|
|
tunnelProvider = tunnel
|
|
super.init()
|
|
}
|
|
|
|
func tunnelConfiguration() -> TunnelConfiguration? {
|
|
return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.tunnelConfiguration()
|
|
}
|
|
|
|
func activateOnDemandSetting() -> ActivateOnDemandSetting {
|
|
return ActivateOnDemandSetting(from: tunnelProvider)
|
|
}
|
|
|
|
func refreshStatus() {
|
|
let status = TunnelStatus(from: tunnelProvider.connection.status)
|
|
self.status = status
|
|
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
|
|
}
|
|
|
|
//swiftlint:disable:next function_body_length
|
|
fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
|
|
if recursionCount >= 8 {
|
|
wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)")
|
|
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedBecauseOfTooManyErrors)
|
|
return
|
|
}
|
|
|
|
wg_log(.debug, message: "startActivation: Entering (tunnel: \(name))")
|
|
|
|
status = .activating // Ensure that no other tunnel can attempt activation until this tunnel is done trying
|
|
|
|
guard tunnelProvider.isEnabled else {
|
|
// In case the tunnel had gotten disabled, re-enable and save it,
|
|
// then call this function again.
|
|
wg_log(.debug, staticMessage: "startActivation: Tunnel is disabled. Re-enabling and saving")
|
|
tunnelProvider.isEnabled = true
|
|
tunnelProvider.saveToPreferences { [weak self] error in
|
|
guard let self = self else { return }
|
|
if error != nil {
|
|
wg_log(.error, message: "Error saving tunnel after re-enabling: \(error!)")
|
|
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileSaving)
|
|
return
|
|
}
|
|
wg_log(.debug, staticMessage: "startActivation: Tunnel saved after re-enabling")
|
|
wg_log(.debug, staticMessage: "startActivation: Invoking startActivation")
|
|
self.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), activationDelegate: activationDelegate)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Start the tunnel
|
|
do {
|
|
wg_log(.debug, staticMessage: "startActivation: Starting tunnel")
|
|
isAttemptingActivation = true
|
|
let activationAttemptId = UUID().uuidString
|
|
self.activationAttemptId = activationAttemptId
|
|
try (tunnelProvider.connection as? NETunnelProviderSession)?.startTunnel(options: ["activationAttemptId": activationAttemptId])
|
|
wg_log(.debug, staticMessage: "startActivation: Success")
|
|
activationDelegate?.tunnelActivationAttemptSucceeded(tunnel: self)
|
|
} catch let error {
|
|
isAttemptingActivation = false
|
|
guard let systemError = error as? NEVPNError else {
|
|
wg_log(.error, message: "Failed to activate tunnel: Error: \(error)")
|
|
status = .inactive
|
|
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting)
|
|
return
|
|
}
|
|
guard systemError.code == NEVPNError.configurationInvalid || systemError.code == NEVPNError.configurationStale else {
|
|
wg_log(.error, message: "Failed to activate tunnel: VPN Error: \(error)")
|
|
status = .inactive
|
|
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting)
|
|
return
|
|
}
|
|
wg_log(.debug, staticMessage: "startActivation: Will reload tunnel and then try to start it.")
|
|
tunnelProvider.loadFromPreferences { [weak self] error in
|
|
guard let self = self else { return }
|
|
if error != nil {
|
|
wg_log(.error, message: "startActivation: Error reloading tunnel: \(error!)")
|
|
self.status = .inactive
|
|
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileLoading)
|
|
return
|
|
}
|
|
wg_log(.debug, staticMessage: "startActivation: Tunnel reloaded")
|
|
wg_log(.debug, staticMessage: "startActivation: Invoking startActivation")
|
|
self.startActivation(recursionCount: recursionCount + 1, lastError: systemError, activationDelegate: activationDelegate)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func startDeactivation() {
|
|
(tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
|
|
}
|
|
}
|