Merge pull request #40 from keeshux/report-tunnel-error-to-host-app

Report tunnel error to host app
This commit is contained in:
Davide De Rosa 2018-10-22 15:47:22 +02:00 committed by GitHub
commit d6526301b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 100 additions and 33 deletions

View File

@ -74,13 +74,13 @@ extension TunnelKitProvider {
public static func deserialized(_ string: String) throws -> EndpointProtocol { public static func deserialized(_ string: String) throws -> EndpointProtocol {
let components = string.components(separatedBy: ":") let components = string.components(separatedBy: ":")
guard components.count == 2 else { guard components.count == 2 else {
throw ProviderError.configuration(field: "endpointProtocol") throw ProviderConfigurationError.parameter(name: "endpointProtocol")
} }
guard let socketType = SocketType(rawValue: components[0]) else { 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 { guard let port = UInt16(components[1]) else {
throw ProviderError.configuration(field: "endpointProtocol.port") throw ProviderConfigurationError.parameter(name: "endpointProtocol.port")
} }
return EndpointProtocol(socketType, port) return EndpointProtocol(socketType, port)
} }
@ -163,6 +163,9 @@ extension TunnelKitProvider {
/// Optional debug log format (SwiftyBeaver format). /// Optional debug log format (SwiftyBeaver format).
public var debugLogFormat: String? public var debugLogFormat: String?
/// The key in `defaults` where to set the raw value of last `TunnelKitProvider.ProviderError`.
public var lastErrorKey: String?
// MARK: Building // MARK: Building
/** /**
@ -188,28 +191,29 @@ extension TunnelKitProvider {
shouldDebug = false shouldDebug = false
debugLogKey = nil debugLogKey = nil
debugLogFormat = nil debugLogFormat = nil
lastErrorKey = nil
} }
fileprivate init(providerConfiguration: [String: Any]) throws { fileprivate init(providerConfiguration: [String: Any]) throws {
let S = Configuration.Keys.self let S = Configuration.Keys.self
guard let cipherAlgorithm = providerConfiguration[S.cipherAlgorithm] as? String, let cipher = SessionProxy.Cipher(rawValue: cipherAlgorithm) else { 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 { 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 ca: CryptoContainer
let clientCertificate: CryptoContainer? let clientCertificate: CryptoContainer?
let clientKey: CryptoContainer? let clientKey: CryptoContainer?
guard let caPEM = providerConfiguration[S.ca] as? String else { 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) ca = CryptoContainer(pem: caPEM)
if let clientPEM = providerConfiguration[S.clientCertificate] as? String { if let clientPEM = providerConfiguration[S.clientCertificate] as? String {
guard let keyPEM = providerConfiguration[S.clientKey] as? String else { 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) clientCertificate = CryptoContainer(pem: clientPEM)
@ -223,7 +227,7 @@ extension TunnelKitProvider {
resolvedAddresses = providerConfiguration[S.resolvedAddresses] as? [String] resolvedAddresses = providerConfiguration[S.resolvedAddresses] as? [String]
guard let endpointProtocolsStrings = providerConfiguration[S.endpointProtocols] as? [String], !endpointProtocolsStrings.isEmpty else { 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) } endpointProtocols = try endpointProtocolsStrings.map { try EndpointProtocol.deserialized($0) }
@ -242,7 +246,7 @@ extension TunnelKitProvider {
do { do {
tlsWrap = try SessionProxy.TLSWrap.deserialized(tlsWrapData) tlsWrap = try SessionProxy.TLSWrap.deserialized(tlsWrapData)
} catch { } catch {
throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.tlsWrap)]") throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.tlsWrap)]")
} }
} }
keepAliveSeconds = providerConfiguration[S.keepAlive] as? Int keepAliveSeconds = providerConfiguration[S.keepAlive] as? Int
@ -252,16 +256,17 @@ extension TunnelKitProvider {
shouldDebug = providerConfiguration[S.debug] as? Bool ?? false shouldDebug = providerConfiguration[S.debug] as? Bool ?? false
if shouldDebug { if shouldDebug {
guard let debugLogKey = providerConfiguration[S.debugLogKey] as? String else { 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 self.debugLogKey = debugLogKey
debugLogFormat = providerConfiguration[S.debugLogFormat] as? String debugLogFormat = providerConfiguration[S.debugLogFormat] as? String
} else { } else {
debugLogKey = nil debugLogKey = nil
} }
lastErrorKey = providerConfiguration[S.lastErrorKey] as? String
guard !prefersResolvedAddresses || !(resolvedAddresses?.isEmpty ?? true) else { 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)]")
} }
} }
@ -288,7 +293,8 @@ extension TunnelKitProvider {
usesPIAPatches: usesPIAPatches, usesPIAPatches: usesPIAPatches,
shouldDebug: shouldDebug, shouldDebug: shouldDebug,
debugLogKey: shouldDebug ? debugLogKey : nil, 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 debugLogKey = "DebugLogKey"
static let debugLogFormat = "DebugLogFormat" static let debugLogFormat = "DebugLogFormat"
static let lastErrorKey = "LastErrorKey"
} }
/// - Seealso: `TunnelKitProvider.ConfigurationBuilder.prefersResolvedAddresses` /// - Seealso: `TunnelKitProvider.ConfigurationBuilder.prefersResolvedAddresses`
@ -384,6 +392,9 @@ extension TunnelKitProvider {
/// - Seealso: `TunnelKitProvider.ConfigurationBuilder.debugLogFormat` /// - Seealso: `TunnelKitProvider.ConfigurationBuilder.debugLogFormat`
public let debugLogFormat: String? public let debugLogFormat: String?
/// - Seealso: `TunnelKitProvider.ConfigurationBuilder.lastErrorKey`
public let lastErrorKey: String?
// MARK: Shortcuts // MARK: Shortcuts
func existingLog(in defaults: UserDefaults) -> [String]? { func existingLog(in defaults: UserDefaults) -> [String]? {
@ -404,7 +415,7 @@ extension TunnelKitProvider {
*/ */
public static func appGroup(from providerConfiguration: [String: Any]) throws -> String { public static func appGroup(from providerConfiguration: [String: Any]) throws -> String {
guard let appGroup = providerConfiguration[Keys.appGroup] as? String else { 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 return appGroup
} }
@ -468,6 +479,9 @@ extension TunnelKitProvider {
if let debugLogFormat = debugLogFormat { if let debugLogFormat = debugLogFormat {
dict[S.debugLogFormat] = debugLogFormat dict[S.debugLogFormat] = debugLogFormat
} }
if let lastErrorKey = lastErrorKey {
dict[S.lastErrorKey] = lastErrorKey
}
return dict return dict
} }
@ -491,7 +505,7 @@ extension TunnelKitProvider {
do { do {
try keychain.set(password: password, for: username, label: Bundle.main.bundleIdentifier) try keychain.set(password: password, for: username, label: Bundle.main.bundleIdentifier)
} catch _ { } catch _ {
throw ProviderError.credentials(field: "keychain.set()") throw ProviderConfigurationError.credentials(details: "keychain.set()")
} }
protocolConfiguration.username = username protocolConfiguration.username = username
protocolConfiguration.passwordReference = try? keychain.passwordReference(for: username) protocolConfiguration.passwordReference = try? keychain.passwordReference(for: username)
@ -562,6 +576,7 @@ extension TunnelKitProvider.Configuration: Equatable {
builder.shouldDebug = shouldDebug builder.shouldDebug = shouldDebug
builder.debugLogKey = debugLogKey builder.debugLogKey = debugLogKey
builder.debugLogFormat = debugLogFormat builder.debugLogFormat = debugLogFormat
builder.lastErrorKey = lastErrorKey
return builder return builder
} }

View File

@ -71,20 +71,24 @@ extension TunnelKitProvider {
} }
} }
/// The errors raised by `TunnelKitProvider`. // mostly programming errors by host app
public enum ProviderError: Error { enum ProviderConfigurationError: Error {
/// The `TunnelKitProvider.Configuration` provided is incorrect or incomplete. /// A field in the `TunnelKitProvider.Configuration` provided is incorrect or incomplete.
case configuration(field: String) case parameter(name: String)
/// Credentials are missing or protected (e.g. device locked). /// Credentials are missing or inaccessible.
case credentials(field: String) case credentials(details: String)
/// The pseudo-random number generator could not be initialized. /// The pseudo-random number generator could not be initialized.
case prngInitialization case prngInitialization
/// The TLS certificate could not be serialized. /// The TLS certificate could not be serialized.
case certificateSerialization case certificateSerialization
}
/// The errors causing a tunnel disconnection.
public enum ProviderError: String, Error {
/// Socket endpoint could not be resolved. /// Socket endpoint could not be resolved.
case dnsFailure case dnsFailure
@ -95,10 +99,22 @@ extension TunnelKitProvider {
/// Socket failed to reach active state. /// Socket failed to reach active state.
case socketActivity case socketActivity
/// Credentials authentication failed.
case authenticationFailed
/// TLS handshake failed.
case tlsFailed
/// Tunnel timed out.
case timeout
/// An error occurred at the link level. /// An error occurred at the link level.
case linkError case linkError
/// The current network changed (e.g. switched from WiFi to data connection). /// The current network changed (e.g. switched from WiFi to data connection).
case networkChanged case networkChanged
/// The server replied in an unexpected way.
case unexpectedReply
} }
} }

View File

@ -121,23 +121,23 @@ open class TunnelKitProvider: NEPacketTunnelProvider {
let hostname: String let hostname: String
do { do {
guard let tunnelProtocol = protocolConfiguration as? NETunnelProviderProtocol else { guard let tunnelProtocol = protocolConfiguration as? NETunnelProviderProtocol else {
throw ProviderError.configuration(field: "protocolConfiguration") throw ProviderConfigurationError.parameter(name: "protocolConfiguration")
} }
guard let serverAddress = tunnelProtocol.serverAddress else { guard let serverAddress = tunnelProtocol.serverAddress else {
throw ProviderError.configuration(field: "protocolConfiguration.serverAddress") throw ProviderConfigurationError.parameter(name: "protocolConfiguration.serverAddress")
} }
guard let providerConfiguration = tunnelProtocol.providerConfiguration else { guard let providerConfiguration = tunnelProtocol.providerConfiguration else {
throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration") throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration")
} }
hostname = serverAddress hostname = serverAddress
try appGroup = Configuration.appGroup(from: providerConfiguration) try appGroup = Configuration.appGroup(from: providerConfiguration)
try cfg = Configuration.parsed(from: providerConfiguration) try cfg = Configuration.parsed(from: providerConfiguration)
} catch let e { } catch let e {
var message: String? var message: String?
if let te = e as? ProviderError { if let te = e as? ProviderConfigurationError {
switch te { switch te {
case .configuration(let field): case .parameter(let name):
message = "Tunnel configuration incomplete: \(field)" message = "Tunnel configuration incomplete: \(name)"
default: default:
break break
@ -176,9 +176,10 @@ open class TunnelKitProvider: NEPacketTunnelProvider {
) )
log.info("Starting tunnel...") log.info("Starting tunnel...")
clearErrorStatus()
guard SessionProxy.EncryptionBridge.prepareRandomNumberGenerator(seedLength: prngSeedLength) else { guard SessionProxy.EncryptionBridge.prepareRandomNumberGenerator(seedLength: prngSeedLength) else {
completionHandler(ProviderError.prngInitialization) completionHandler(ProviderConfigurationError.prngInitialization)
return return
} }
@ -190,7 +191,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider {
try cfg.ca.write(to: url) try cfg.ca.write(to: url)
caPath = url.path caPath = url.path
} catch { } catch {
completionHandler(ProviderError.certificateSerialization) completionHandler(ProviderConfigurationError.certificateSerialization)
return return
} }
if let clientCertificate = cfg.clientCertificate { if let clientCertificate = cfg.clientCertificate {
@ -199,7 +200,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider {
try clientCertificate.write(to: url) try clientCertificate.write(to: url)
clientCertificatePath = url.path clientCertificatePath = url.path
} catch { } catch {
completionHandler(ProviderError.certificateSerialization) completionHandler(ProviderConfigurationError.certificateSerialization)
return return
} }
} else { } else {
@ -211,7 +212,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider {
try clientKey.write(to: url) try clientKey.write(to: url)
clientKeyPath = url.path clientKeyPath = url.path
} catch { } catch {
completionHandler(ProviderError.certificateSerialization) completionHandler(ProviderConfigurationError.certificateSerialization)
return return
} }
} else { } else {
@ -259,6 +260,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider {
open override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { open override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
pendingStartHandler = nil pendingStartHandler = nil
log.info("Stopping tunnel...") log.info("Stopping tunnel...")
clearErrorStatus()
guard let proxy = proxy else { guard let proxy = proxy else {
flushLog() flushLog()
@ -324,6 +326,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider {
private func connectTunnel(via socket: GenericSocket) { private func connectTunnel(via socket: GenericSocket) {
log.info("Will connect to \(socket)") log.info("Will connect to \(socket)")
clearErrorStatus()
log.debug("Socket type is \(type(of: socket))") log.debug("Socket type is \(type(of: socket))")
self.socket = socket self.socket = socket
@ -342,6 +345,7 @@ open class TunnelKitProvider: NEPacketTunnelProvider {
if let error = error { if let error = error {
log.error("Tunnel did stop (error: \(error))") log.error("Tunnel did stop (error: \(error))")
setErrorStatus(with: error)
} else { } else {
log.info("Tunnel did stop on request") log.info("Tunnel did stop on request")
} }
@ -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() { private func logCurrentSSID() {
if let ssid = observer.currentWifiNetworkName() { if let ssid = observer.currentWifiNetworkName() {
log.debug("Current SSID: '\(ssid)'") log.debug("Current SSID: '\(ssid)'")

View File

@ -38,7 +38,7 @@
import Foundation import Foundation
/// The possible errors raised/thrown during `SessionProxy` operation. /// The possible errors raised/thrown during `SessionProxy` operation.
public enum SessionError: Error { public enum SessionError: String, Error {
/// The negotiation timed out. /// The negotiation timed out.
case negotiationTimeout case negotiationTimeout