From 6200a0bc1c9e92ef4b290d3a740484b010f1b5da Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 21 Oct 2018 12:58:36 +0200 Subject: [PATCH 1/4] Split configuration and session errors --- .../TunnelKitProvider+Configuration.swift | 26 +++++++++---------- .../TunnelKitProvider+Interaction.swift | 18 ++++++++----- .../AppExtension/TunnelKitProvider.swift | 20 +++++++------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift index a0c0c91..d3aa38a 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift @@ -74,13 +74,13 @@ extension TunnelKitProvider { public static func deserialized(_ string: String) throws -> EndpointProtocol { let components = string.components(separatedBy: ":") guard components.count == 2 else { - throw ProviderError.configuration(field: "endpointProtocol") + throw ProviderConfigurationError.parameter(name: "endpointProtocol") } guard let socketType = SocketType(rawValue: components[0]) else { - throw ProviderError.configuration(field: "endpointProtocol.socketType") + throw ProviderConfigurationError.parameter(name: "endpointProtocol.socketType") } guard let port = UInt16(components[1]) else { - throw ProviderError.configuration(field: "endpointProtocol.port") + throw ProviderConfigurationError.parameter(name: "endpointProtocol.port") } return EndpointProtocol(socketType, port) } @@ -194,22 +194,22 @@ extension TunnelKitProvider { let S = Configuration.Keys.self guard let cipherAlgorithm = providerConfiguration[S.cipherAlgorithm] as? String, let cipher = SessionProxy.Cipher(rawValue: cipherAlgorithm) else { - throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.cipherAlgorithm)]") + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.cipherAlgorithm)]") } guard let digestAlgorithm = providerConfiguration[S.digestAlgorithm] as? String, let digest = SessionProxy.Digest(rawValue: digestAlgorithm) else { - throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.digestAlgorithm)]") + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.digestAlgorithm)]") } let ca: CryptoContainer let clientCertificate: CryptoContainer? let clientKey: CryptoContainer? guard let caPEM = providerConfiguration[S.ca] as? String else { - throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.ca)]") + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.ca)]") } ca = CryptoContainer(pem: caPEM) if let clientPEM = providerConfiguration[S.clientCertificate] as? String { guard let keyPEM = providerConfiguration[S.clientKey] as? String else { - throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.clientKey)]") + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.clientKey)]") } clientCertificate = CryptoContainer(pem: clientPEM) @@ -223,7 +223,7 @@ extension TunnelKitProvider { resolvedAddresses = providerConfiguration[S.resolvedAddresses] as? [String] guard let endpointProtocolsStrings = providerConfiguration[S.endpointProtocols] as? [String], !endpointProtocolsStrings.isEmpty else { - throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.endpointProtocols)] is nil or empty") + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.endpointProtocols)] is nil or empty") } endpointProtocols = try endpointProtocolsStrings.map { try EndpointProtocol.deserialized($0) } @@ -242,7 +242,7 @@ extension TunnelKitProvider { do { tlsWrap = try SessionProxy.TLSWrap.deserialized(tlsWrapData) } catch { - throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.tlsWrap)]") + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.tlsWrap)]") } } keepAliveSeconds = providerConfiguration[S.keepAlive] as? Int @@ -252,7 +252,7 @@ extension TunnelKitProvider { shouldDebug = providerConfiguration[S.debug] as? Bool ?? false if shouldDebug { guard let debugLogKey = providerConfiguration[S.debugLogKey] as? String else { - throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.debugLogKey)]") + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.debugLogKey)]") } self.debugLogKey = debugLogKey debugLogFormat = providerConfiguration[S.debugLogFormat] as? String @@ -261,7 +261,7 @@ extension TunnelKitProvider { } guard !prefersResolvedAddresses || !(resolvedAddresses?.isEmpty ?? true) else { - throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.prefersResolvedAddresses)] is true but no [\(S.resolvedAddresses)]") + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.prefersResolvedAddresses)] is true but no [\(S.resolvedAddresses)]") } } @@ -404,7 +404,7 @@ extension TunnelKitProvider { */ public static func appGroup(from providerConfiguration: [String: Any]) throws -> String { guard let appGroup = providerConfiguration[Keys.appGroup] as? String else { - throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(Keys.appGroup)]") + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(Keys.appGroup)]") } return appGroup } @@ -491,7 +491,7 @@ extension TunnelKitProvider { do { try keychain.set(password: password, for: username, label: Bundle.main.bundleIdentifier) } catch _ { - throw ProviderError.credentials(field: "keychain.set()") + throw ProviderConfigurationError.credentials(details: "keychain.set()") } protocolConfiguration.username = username protocolConfiguration.passwordReference = try? keychain.passwordReference(for: username) diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Interaction.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Interaction.swift index 73a88d1..1dd539f 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Interaction.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Interaction.swift @@ -71,20 +71,24 @@ extension TunnelKitProvider { } } - /// The errors raised by `TunnelKitProvider`. - public enum ProviderError: Error { - - /// The `TunnelKitProvider.Configuration` provided is incorrect or incomplete. - case configuration(field: String) + // mostly programming errors by host app + enum ProviderConfigurationError: Error { - /// Credentials are missing or protected (e.g. device locked). - case credentials(field: String) + /// A field in the `TunnelKitProvider.Configuration` provided is incorrect or incomplete. + case parameter(name: String) + + /// Credentials are missing or inaccessible. + case credentials(details: String) /// The pseudo-random number generator could not be initialized. case prngInitialization /// The TLS certificate could not be serialized. case certificateSerialization + } + + /// The errors causing a tunnel disconnection. + public enum ProviderError: String, Error { /// Socket endpoint could not be resolved. case dnsFailure diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift index 348adaa..d3c69bd 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift @@ -121,23 +121,23 @@ open class TunnelKitProvider: NEPacketTunnelProvider { let hostname: String do { guard let tunnelProtocol = protocolConfiguration as? NETunnelProviderProtocol else { - throw ProviderError.configuration(field: "protocolConfiguration") + throw ProviderConfigurationError.parameter(name: "protocolConfiguration") } guard let serverAddress = tunnelProtocol.serverAddress else { - throw ProviderError.configuration(field: "protocolConfiguration.serverAddress") + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.serverAddress") } guard let providerConfiguration = tunnelProtocol.providerConfiguration else { - throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration") + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration") } hostname = serverAddress try appGroup = Configuration.appGroup(from: providerConfiguration) try cfg = Configuration.parsed(from: providerConfiguration) } catch let e { var message: String? - if let te = e as? ProviderError { + if let te = e as? ProviderConfigurationError { switch te { - case .configuration(let field): - message = "Tunnel configuration incomplete: \(field)" + case .parameter(let name): + message = "Tunnel configuration incomplete: \(name)" default: break @@ -178,7 +178,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider { log.info("Starting tunnel...") guard SessionProxy.EncryptionBridge.prepareRandomNumberGenerator(seedLength: prngSeedLength) else { - completionHandler(ProviderError.prngInitialization) + completionHandler(ProviderConfigurationError.prngInitialization) return } @@ -190,7 +190,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider { try cfg.ca.write(to: url) caPath = url.path } catch { - completionHandler(ProviderError.certificateSerialization) + completionHandler(ProviderConfigurationError.certificateSerialization) return } if let clientCertificate = cfg.clientCertificate { @@ -199,7 +199,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider { try clientCertificate.write(to: url) clientCertificatePath = url.path } catch { - completionHandler(ProviderError.certificateSerialization) + completionHandler(ProviderConfigurationError.certificateSerialization) return } } else { @@ -211,7 +211,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider { try clientKey.write(to: url) clientKeyPath = url.path } catch { - completionHandler(ProviderError.certificateSerialization) + completionHandler(ProviderConfigurationError.certificateSerialization) return } } else { From 4bf7f1a1fc2f02b8d27f7cd8e5c0c5b8a740573c Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 21 Oct 2018 13:42:21 +0200 Subject: [PATCH 2/4] Bridge SessionError to public ProviderError --- .../AppExtension/TunnelKitProvider+Interaction.swift | 12 ++++++++++++ TunnelKit/Sources/Core/SessionError.swift | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Interaction.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Interaction.swift index 1dd539f..e035a35 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Interaction.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Interaction.swift @@ -99,10 +99,22 @@ extension TunnelKitProvider { /// Socket failed to reach active state. case socketActivity + /// Credentials authentication failed. + case authenticationFailed + + /// TLS handshake failed. + case tlsFailed + + /// Tunnel timed out. + case timeout + /// An error occurred at the link level. case linkError /// The current network changed (e.g. switched from WiFi to data connection). case networkChanged + + /// The server replied in an unexpected way. + case unexpectedReply } } diff --git a/TunnelKit/Sources/Core/SessionError.swift b/TunnelKit/Sources/Core/SessionError.swift index 90e8656..4f37b6d 100644 --- a/TunnelKit/Sources/Core/SessionError.swift +++ b/TunnelKit/Sources/Core/SessionError.swift @@ -38,7 +38,7 @@ import Foundation /// The possible errors raised/thrown during `SessionProxy` operation. -public enum SessionError: Error { +public enum SessionError: String, Error { /// The negotiation timed out. case negotiationTimeout From 7ffb997904871b8b817768ab221ce74f7338616f Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 21 Oct 2018 23:36:23 +0200 Subject: [PATCH 3/4] Add defaults key for last error --- .../TunnelKitProvider+Configuration.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift index d3aa38a..5ac1e7d 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift @@ -163,6 +163,9 @@ extension TunnelKitProvider { /// Optional debug log format (SwiftyBeaver format). public var debugLogFormat: String? + /// The key in `defaults` where to set the raw value of last `TunnelKitProvider.ProviderError`. + public var lastErrorKey: String? + // MARK: Building /** @@ -188,6 +191,7 @@ extension TunnelKitProvider { shouldDebug = false debugLogKey = nil debugLogFormat = nil + lastErrorKey = nil } fileprivate init(providerConfiguration: [String: Any]) throws { @@ -259,6 +263,7 @@ extension TunnelKitProvider { } else { debugLogKey = nil } + lastErrorKey = providerConfiguration[S.lastErrorKey] as? String guard !prefersResolvedAddresses || !(resolvedAddresses?.isEmpty ?? true) else { throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.prefersResolvedAddresses)] is true but no [\(S.resolvedAddresses)]") @@ -288,7 +293,8 @@ extension TunnelKitProvider { usesPIAPatches: usesPIAPatches, shouldDebug: shouldDebug, debugLogKey: shouldDebug ? debugLogKey : nil, - debugLogFormat: shouldDebug ? debugLogFormat : nil + debugLogFormat: shouldDebug ? debugLogFormat : nil, + lastErrorKey: lastErrorKey ) } } @@ -331,6 +337,8 @@ extension TunnelKitProvider { static let debugLogKey = "DebugLogKey" static let debugLogFormat = "DebugLogFormat" + + static let lastErrorKey = "LastErrorKey" } /// - Seealso: `TunnelKitProvider.ConfigurationBuilder.prefersResolvedAddresses` @@ -384,6 +392,9 @@ extension TunnelKitProvider { /// - Seealso: `TunnelKitProvider.ConfigurationBuilder.debugLogFormat` public let debugLogFormat: String? + /// - Seealso: `TunnelKitProvider.ConfigurationBuilder.lastErrorKey` + public let lastErrorKey: String? + // MARK: Shortcuts func existingLog(in defaults: UserDefaults) -> [String]? { @@ -468,6 +479,9 @@ extension TunnelKitProvider { if let debugLogFormat = debugLogFormat { dict[S.debugLogFormat] = debugLogFormat } + if let lastErrorKey = lastErrorKey { + dict[S.lastErrorKey] = lastErrorKey + } return dict } @@ -562,6 +576,7 @@ extension TunnelKitProvider.Configuration: Equatable { builder.shouldDebug = shouldDebug builder.debugLogKey = debugLogKey builder.debugLogFormat = debugLogFormat + builder.lastErrorKey = lastErrorKey return builder } From 1ad4a62593a6666c9068f02d0e02f69b7da7038a Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 21 Oct 2018 23:30:55 +0200 Subject: [PATCH 4/4] Report error status to shared defaults Retain after disposal, unless manually stopped. --- .../AppExtension/TunnelKitProvider.swift | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift index d3c69bd..71ba869 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift @@ -176,6 +176,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider { ) log.info("Starting tunnel...") + clearErrorStatus() guard SessionProxy.EncryptionBridge.prepareRandomNumberGenerator(seedLength: prngSeedLength) else { completionHandler(ProviderConfigurationError.prngInitialization) @@ -259,6 +260,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider { open override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { pendingStartHandler = nil log.info("Stopping tunnel...") + clearErrorStatus() guard let proxy = proxy else { flushLog() @@ -324,6 +326,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider { private func connectTunnel(via socket: GenericSocket) { log.info("Will connect to \(socket)") + clearErrorStatus() log.debug("Socket type is \(type(of: socket))") self.socket = socket @@ -342,6 +345,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider { if let error = error { log.error("Tunnel did stop (error: \(error))") + setErrorStatus(with: error) } else { log.info("Tunnel did stop on request") } @@ -349,7 +353,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider { private func disposeTunnel(error: Error?) { flushLog() - + // failed to start if (pendingStartHandler != nil) { @@ -599,6 +603,38 @@ extension TunnelKitProvider { } } + private func setErrorStatus(with error: Error) { + guard let lastErrorKey = cfg.lastErrorKey else { + return + } + let providerError: ProviderError + if let se = error as? SessionError { + switch se { + case .badCredentials: + providerError = .authenticationFailed + + case .peerVerification, .tlsError: + providerError = .tlsFailed + + case .negotiationTimeout, .pingTimeout: + providerError = .timeout + + default: + providerError = .unexpectedReply + } + } else { + providerError = error as? ProviderError ?? .linkError + } + defaults?.set(providerError.rawValue, forKey: lastErrorKey) + } + + private func clearErrorStatus() { + guard let lastErrorKey = cfg.lastErrorKey else { + return + } + defaults?.removeObject(forKey: lastErrorKey) + } + private func logCurrentSSID() { if let ssid = observer.currentWifiNetworkName() { log.debug("Current SSID: '\(ssid)'")