// 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) -> Void) { #if targetEnvironment(simulator) completionHandler(.success(TunnelsManager(tunnelProviders: []))) #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) -> 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, 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 { if tunnels.contains(where: { $0.name == tunnelName }) { 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 } tunnel.startActivation(activationDelegate: activationDelegate) } func startDeactivation(of tunnel: TunnelContainer) { tunnel.isAttemptingActivation = false guard tunnel.status != .inactive && tunnel.status != .deactivating else { return } tunnel.startDeactivation() } 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() } } extension NEVPNStatus: CustomDebugStringConvertible { public var debugDescription: String { switch self { case .connected: return "connected" case .connecting: return "connecting" case .disconnected: return "disconnected" case .disconnecting: return "disconnecting" case .reasserting: return "reasserting" case .invalid: return "invalid" } } }