TunnelsManager: Handle status change in TunnelsManager

Rather than in TunnelContainer.
This commit is contained in:
Roopesh Chander 2018-12-10 17:04:24 +05:30
parent 305264d064
commit 9906689122
1 changed files with 78 additions and 66 deletions

View File

@ -27,6 +27,7 @@ enum TunnelsManagerError: WireGuardAppError {
// Tunnel activation // Tunnel activation
case attemptingActivationWhenTunnelIsNotInactive case attemptingActivationWhenTunnelIsNotInactive
case tunnelActivationSupercededWhileWaiting // Another tunnel activation was initiated while this tunnel was waiting
case tunnelActivationAttemptFailed // startTunnel() throwed case tunnelActivationAttemptFailed // startTunnel() throwed
case tunnelActivationFailedInternalError // startTunnel() succeeded, but activation failed case tunnelActivationFailedInternalError // startTunnel() succeeded, but activation failed
case tunnelActivationFailedNoInternetConnection // startTunnel() succeeded, but activation failed since no internet case tunnelActivationFailedNoInternetConnection // startTunnel() succeeded, but activation failed since no internet
@ -48,6 +49,8 @@ enum TunnelsManagerError: WireGuardAppError {
case .attemptingActivationWhenTunnelIsNotInactive: case .attemptingActivationWhenTunnelIsNotInactive:
return ("Activation failure", "The tunnel is already active or in the process of being activated") return ("Activation failure", "The tunnel is already active or in the process of being activated")
case .tunnelActivationSupercededWhileWaiting:
return nil
case .tunnelActivationAttemptFailed: case .tunnelActivationAttemptFailed:
return ("Activation failure", "The tunnel could not be activated due to an internal error") return ("Activation failure", "The tunnel could not be activated due to an internal error")
case .tunnelActivationFailedInternalError: case .tunnelActivationFailedInternalError:
@ -65,6 +68,9 @@ class TunnelsManager {
weak var activationDelegate: TunnelsManagerActivationDelegate? weak var activationDelegate: TunnelsManagerActivationDelegate?
private var statusObservationToken: AnyObject? private var statusObservationToken: AnyObject?
var tunnelBeingActivated: TunnelContainer?
var tunnelWaitingForActivation: (tunnel: TunnelContainer, completionHandler: (TunnelsManagerError?) -> Void)?
init(tunnelProviders: [NETunnelProviderManager]) { init(tunnelProviders: [NETunnelProviderManager]) {
self.tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name } self.tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
self.startObservingTunnelStatuses() self.startObservingTunnelStatuses()
@ -181,7 +187,9 @@ class TunnelsManager {
if (tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting) { 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 // Turn off the tunnel, and then turn it back on, so the changes are made effective
tunnel.beginRestart() let session = (tunnel.tunnelProvider.connection as! NETunnelProviderSession)
tunnel.status = .restarting
session.stopTunnel()
} }
if (isActivatingOnDemand) { if (isActivatingOnDemand) {
@ -234,36 +242,43 @@ class TunnelsManager {
} }
func startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) { func startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
guard tunnels.contains(tunnel) else { return } // Ensure it's not deleted
guard (tunnel.status == .inactive) else { guard (tunnel.status == .inactive) else {
completionHandler(TunnelsManagerError.attemptingActivationWhenTunnelIsNotInactive) completionHandler(TunnelsManagerError.attemptingActivationWhenTunnelIsNotInactive)
return return
} }
func _startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) { if let (waitingTunnel, waitingTunnelCompletionHandler) = self.tunnelWaitingForActivation {
tunnel.onActivationCommitted = { [weak self] (success) in precondition(waitingTunnel.status == .waiting)
if (!success) { self.tunnelWaitingForActivation = nil
let error = (InternetReachability.currentStatus() == .notReachable ? waitingTunnel.status = .inactive
TunnelsManagerError.tunnelActivationFailedNoInternetConnection : waitingTunnelCompletionHandler(TunnelsManagerError.tunnelActivationSupercededWhileWaiting)
TunnelsManagerError.tunnelActivationFailedInternalError)
self?.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: error)
}
}
tunnel.startActivation(completionHandler: completionHandler)
} }
if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) { if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive && $0.status != .waiting }) {
tunnel.status = .waiting tunnel.status = .waiting
tunnelInOperation.onDeactivationComplete = { tunnelWaitingForActivation = (tunnel, completionHandler)
_startActivation(of: tunnel, completionHandler: completionHandler) os_log("Tunnel '%{public}@' is waiting for deactivation of '%{public}@' (status: %{public}@)",
} log: OSLog.default, type: .debug, tunnel.name, tunnelInOperation.name, "\(tunnelInOperation.status)")
if (tunnelInOperation.status != .deactivating) {
tunnelBeingActivated = nil
startDeactivation(of: tunnelInOperation) startDeactivation(of: tunnelInOperation)
}
} else { } else {
_startActivation(of: tunnel, completionHandler: completionHandler) tunnelBeingActivated = tunnel
tunnel.startActivation(completionHandler: completionHandler)
} }
} }
func startDeactivation(of tunnel: TunnelContainer) { func startDeactivation(of tunnel: TunnelContainer) {
if (tunnel.status == .inactive) { if (tunnel.status == .waiting) {
let inferredStatus = TunnelStatus(from: tunnel.tunnelProvider.connection.status)
if (inferredStatus == .inactive) {
tunnel.status = .inactive
return
}
}
if (tunnel.status == .inactive || tunnel.status == .deactivating) {
return return
} }
tunnel.startDeactivation() tunnel.startDeactivation()
@ -280,15 +295,57 @@ class TunnelsManager {
statusObservationToken = NotificationCenter.default.addObserver( statusObservationToken = NotificationCenter.default.addObserver(
forName: .NEVPNStatusDidChange, forName: .NEVPNStatusDidChange,
object: nil, object: nil,
queue: nil) { [weak self] (statusChangeNotification) in queue: OperationQueue.main) { [weak self] (statusChangeNotification) in
guard let session = statusChangeNotification.object as? NETunnelProviderSession else { return } guard let session = statusChangeNotification.object as? NETunnelProviderSession else { return }
guard let tunnelProvider = session.manager as? NETunnelProviderManager else { return } guard let tunnelProvider = session.manager as? NETunnelProviderManager else { return }
if let tunnel = self?.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) { guard let tunnel = self?.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
tunnel.tunnelConnectionStatusDidChange() guard let s = self else { return }
// In case our attempt to start the tunnel, didn't succeed
if (tunnel == s.tunnelBeingActivated) {
if (session.status == .disconnected) {
let error = (InternetReachability.currentStatus() == .notReachable ?
TunnelsManagerError.tunnelActivationFailedNoInternetConnection :
TunnelsManagerError.tunnelActivationFailedInternalError)
s.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: error)
s.tunnelBeingActivated = nil
} else if (session.status == .connected) {
s.tunnelBeingActivated = nil
}
}
// 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) {
s.tunnelBeingActivated = tunnel
tunnel.startActivation(completionHandler: { _ in })
}
return
}
// Update tunnel status
tunnel.refreshStatus()
// In case some other tunnel is waiting on this tunnel's deactivation
if (tunnel.status == .inactive) {
if let (waitingTunnel, waitingTunnelCompletionHandler) = s.tunnelWaitingForActivation {
os_log("Activating waiting tunnel '%{public}@' after deactivation of '%{public}@'",
log: OSLog.default, type: .debug, waitingTunnel.name, tunnel.name)
precondition(waitingTunnel.status == .waiting)
s.tunnelWaitingForActivation = nil
s.tunnelBeingActivated = waitingTunnel
waitingTunnel.startActivation(completionHandler: waitingTunnelCompletionHandler)
}
} }
} }
} }
deinit {
if let statusObservationToken = self.statusObservationToken {
NotificationCenter.default.removeObserver(statusObservationToken)
}
}
} }
class TunnelContainer: NSObject { class TunnelContainer: NSObject {
@ -332,8 +389,6 @@ class TunnelContainer: NSObject {
guard let tunnelConfiguration = tunnelConfiguration() else { fatalError() } guard let tunnelConfiguration = tunnelConfiguration() else { fatalError() }
onDeactivationComplete = nil
isAttemptingActivation = true
startActivation(tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler) startActivation(tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler)
} }
@ -347,7 +402,7 @@ class TunnelContainer: NSObject {
return return
} }
os_log("startActivation: Entering", log: OSLog.default, type: .debug) os_log("startActivation: Entering (tunnel: %{public}@)", log: OSLog.default, type: .debug, self.name)
guard (tunnelProvider.isEnabled) else { guard (tunnelProvider.isEnabled) else {
// In case the tunnel had gotten disabled, re-enable and save it, // In case the tunnel had gotten disabled, re-enable and save it,
@ -413,49 +468,6 @@ class TunnelContainer: NSObject {
} }
session.stopTunnel() session.stopTunnel()
} }
fileprivate func beginRestart() {
assert(status == .active || status == .activating || status == .reasserting)
status = .restarting
let session = (tunnelProvider.connection as! NETunnelProviderSession)
session.stopTunnel()
}
fileprivate func tunnelConnectionStatusDidChange() {
let connection = tunnelProvider.connection
// Avoid acting on duplicate notifications of status change
if let lastTunnelConnectionStatus = self.lastTunnelConnectionStatus {
if (lastTunnelConnectionStatus == connection.status) {
return
}
}
self.lastTunnelConnectionStatus = connection.status
// Act on the status change
if (self.isAttemptingActivation) {
if (connection.status == .connecting || connection.status == .connected) {
// We tried to start the tunnel, and that attempt is on track to become succeessful
self.onActivationCommitted?(true)
self.onActivationCommitted = nil
} else if (connection.status == .disconnecting || connection.status == .disconnected) {
// We tried to start the tunnel, but that attempt didn't succeed
self.onActivationCommitted?(false)
self.onActivationCommitted = nil
}
self.isAttemptingActivation = false
}
if ((self.status == .restarting) && (connection.status == .disconnected || connection.status == .disconnecting)) {
// Don't change self.status when disconnecting for a restart
if (connection.status == .disconnected) {
self.startActivation(completionHandler: { _ in })
}
return
}
self.status = TunnelStatus(from: connection.status)
if (self.status == .inactive) {
self.onDeactivationComplete?()
self.onDeactivationComplete = nil
}
}
} }
@objc enum TunnelStatus: Int { @objc enum TunnelStatus: Int {