NE: Communicate last error to app through a shared file

This commit is contained in:
Roopesh Chander 2018-12-14 02:24:53 +05:30
parent 206de837d1
commit e6c1e46b1d
4 changed files with 84 additions and 16 deletions

View File

@ -17,6 +17,18 @@ extension FileManager {
return sharedFolderURL.appendingPathComponent("tunnel-log.txt") return sharedFolderURL.appendingPathComponent("tunnel-log.txt")
} }
static var networkExtensionLastErrorFileURL: URL? {
guard let appGroupId = Bundle.main.object(forInfoDictionaryKey: "com.wireguard.ios.app_group_id") as? String else {
os_log("Can't obtain app group id from bundle", log: OSLog.default, type: .error)
return nil
}
guard let sharedFolderURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else {
os_log("Can't obtain shared folder URL", log: OSLog.default, type: .error)
return nil
}
return sharedFolderURL.appendingPathComponent("last-error.txt")
}
static var appLogFileURL: URL? { static var appLogFileURL: URL? {
guard let documentDirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { guard let documentDirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
os_log("Can't obtain app documents folder URL", log: OSLog.default, type: .error) os_log("Can't obtain app documents folder URL", log: OSLog.default, type: .error)

View File

@ -41,8 +41,14 @@ enum TunnelsManagerActivationAttemptError: WireGuardAppError {
enum TunnelsManagerActivationError: WireGuardAppError { enum TunnelsManagerActivationError: WireGuardAppError {
case activationFailed case activationFailed
case activationFailedWithExtensionError(title: String, message: String)
var alertText: AlertText { var alertText: AlertText {
switch self {
case .activationFailed:
return ("Activation failure", "The tunnel could not be activated. Please ensure you are connected to the Internet.") return ("Activation failure", "The tunnel could not be activated. Please ensure you are connected to the Internet.")
case .activationFailedWithExtensionError(let title, let message):
return (title, message)
}
} }
} }
@ -297,9 +303,13 @@ class TunnelsManager {
self.activationDelegate?.tunnelActivationSucceeded(tunnel: tunnel) self.activationDelegate?.tunnelActivationSucceeded(tunnel: tunnel)
} else if session.status == .disconnected { } else if session.status == .disconnected {
tunnel.isAttemptingActivation = false 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) self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailed)
} }
} }
}
// In case we're restarting the tunnel // In case we're restarting the tunnel
if (tunnel.status == .restarting) && (session.status == .disconnected || session.status == .disconnecting) { if (tunnel.status == .restarting) && (session.status == .disconnected || session.status == .disconnecting) {
@ -321,6 +331,20 @@ class TunnelsManager {
} }
} }
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 { deinit {
if let statusObservationToken = self.statusObservationToken { if let statusObservationToken = self.statusObservationToken {
NotificationCenter.default.removeObserver(statusObservationToken) NotificationCenter.default.removeObserver(statusObservationToken)
@ -335,6 +359,7 @@ class TunnelContainer: NSObject {
@objc dynamic var isActivateOnDemandEnabled: Bool @objc dynamic var isActivateOnDemandEnabled: Bool
var isAttemptingActivation = false var isAttemptingActivation = false
var activationAttemptId: String?
fileprivate let tunnelProvider: NETunnelProviderManager fileprivate let tunnelProvider: NETunnelProviderManager
private var lastTunnelConnectionStatus: NEVPNStatus? private var lastTunnelConnectionStatus: NEVPNStatus?
@ -398,7 +423,9 @@ class TunnelContainer: NSObject {
do { do {
wg_log(.debug, staticMessage: "startActivation: Starting tunnel") wg_log(.debug, staticMessage: "startActivation: Starting tunnel")
self.isAttemptingActivation = true self.isAttemptingActivation = true
try (tunnelProvider.connection as? NETunnelProviderSession)?.startTunnel() let activationAttemptId = UUID().uuidString
self.activationAttemptId = activationAttemptId
try (tunnelProvider.connection as? NETunnelProviderSession)?.startTunnel(options: ["activationAttemptId": activationAttemptId])
wg_log(.debug, staticMessage: "startActivation: Success") wg_log(.debug, staticMessage: "startActivation: Success")
activationDelegate?.tunnelActivationAttemptSucceeded(tunnel: self) activationDelegate?.tunnelActivationAttemptSucceeded(tunnel: self)
} catch let error { } catch let error {

View File

@ -4,7 +4,17 @@
import NetworkExtension import NetworkExtension
class ErrorNotifier { class ErrorNotifier {
static func errorMessage(for error: PacketTunnelProviderError) -> (String, String)? {
let activationAttemptId: String?
weak var tunnelProvider: NEPacketTunnelProvider?
init(activationAttemptId: String?, tunnelProvider: NEPacketTunnelProvider) {
self.activationAttemptId = activationAttemptId
self.tunnelProvider = tunnelProvider
ErrorNotifier.removeLastErrorFile()
}
func errorMessage(for error: PacketTunnelProviderError) -> (String, String)? {
switch error { switch error {
case .savedProtocolConfigurationIsInvalid: case .savedProtocolConfigurationIsInvalid:
return ("Activation failure", "Could not retrieve tunnel information from the saved configuration") return ("Activation failure", "Could not retrieve tunnel information from the saved configuration")
@ -17,9 +27,24 @@ class ErrorNotifier {
} }
} }
static func notify(_ error: PacketTunnelProviderError, from tunnelProvider: NEPacketTunnelProvider) { func notify(_ error: PacketTunnelProviderError) {
guard let (title, message) = ErrorNotifier.errorMessage(for: error) else { return } guard let (title, message) = errorMessage(for: error) else { return }
// displayMessage() is deprecated, but there's no better alternative to show the error to the user if let activationAttemptId = activationAttemptId, let lastErrorFilePath = FileManager.networkExtensionLastErrorFileURL?.path {
// The tunnel was started from the app
let errorMessageData = "\(activationAttemptId)\n\(title)\n\(message)".data(using: .utf8)
FileManager.default.createFile(atPath: lastErrorFilePath, contents: errorMessageData, attributes: nil)
} else {
// The tunnel was probably started from iOS Settings app
if let tunnelProvider = self.tunnelProvider {
// displayMessage() is deprecated, but there's no better alternative if invoked from iOS Settings
tunnelProvider.displayMessage("\(title): \(message)") { _ in } tunnelProvider.displayMessage("\(title): \(message)") { _ in }
} }
} }
}
static func removeLastErrorFile() {
if let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL {
_ = FileManager.deleteFile(at: lastErrorFileURL)
}
}
}

View File

@ -28,21 +28,23 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
networkMonitor?.cancel() networkMonitor?.cancel()
} }
/// Begin the process of establishing the tunnel.
override func startTunnel(options: [String: NSObject]?, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) { override func startTunnel(options: [String: NSObject]?, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
let activationAttemptId = options?["activationAttemptId"] as? String
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId, tunnelProvider: self)
guard let tunnelProviderProtocol = self.protocolConfiguration as? NETunnelProviderProtocol, guard let tunnelProviderProtocol = self.protocolConfiguration as? NETunnelProviderProtocol,
let tunnelConfiguration = tunnelProviderProtocol.tunnelConfiguration() else { let tunnelConfiguration = tunnelProviderProtocol.tunnelConfiguration() else {
ErrorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid, from: self) errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid) startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
return return
} }
startTunnel(with: tunnelConfiguration, completionHandler: startTunnelCompletionHandler) startTunnel(with: tunnelConfiguration, errorNotifier: errorNotifier, completionHandler: startTunnelCompletionHandler)
} }
//swiftlint:disable:next function_body_length //swiftlint:disable:next function_body_length
func startTunnel(with tunnelConfiguration: TunnelConfiguration, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) { func startTunnel(with tunnelConfiguration: TunnelConfiguration, errorNotifier: ErrorNotifier, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
configureLogger() configureLogger()
@ -55,7 +57,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
} catch DNSResolverError.dnsResolutionFailed(let hostnames) { } catch DNSResolverError.dnsResolutionFailed(let hostnames) {
wg_log(.error, staticMessage: "Starting tunnel failed: DNS resolution failure") wg_log(.error, staticMessage: "Starting tunnel failed: DNS resolution failure")
wg_log(.error, message: "Hostnames for which DNS resolution failed: \(hostnames.joined(separator: ", "))") wg_log(.error, message: "Hostnames for which DNS resolution failed: \(hostnames.joined(separator: ", "))")
ErrorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames), from: self) errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames))
startTunnelCompletionHandler(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames)) startTunnelCompletionHandler(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames))
return return
} catch { } catch {
@ -73,7 +75,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
let fileDescriptor = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32 //swiftlint:disable:this force_cast let fileDescriptor = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32 //swiftlint:disable:this force_cast
if fileDescriptor < 0 { if fileDescriptor < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not determine file descriptor") wg_log(.error, staticMessage: "Starting tunnel failed: Could not determine file descriptor")
ErrorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard, from: self) errorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard) startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return return
} }
@ -101,7 +103,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
if handle < 0 { if handle < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not start WireGuard") wg_log(.error, staticMessage: "Starting tunnel failed: Could not start WireGuard")
ErrorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard, from: self) errorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard) startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return return
} }
@ -115,7 +117,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
if let error = error { if let error = error {
wg_log(.error, staticMessage: "Starting tunnel failed: Error setting network settings.") wg_log(.error, staticMessage: "Starting tunnel failed: Error setting network settings.")
wg_log(.error, message: "Error from setTunnelNetworkSettings: \(error.localizedDescription)") wg_log(.error, message: "Error from setTunnelNetworkSettings: \(error.localizedDescription)")
ErrorNotifier.notify(PacketTunnelProviderError.coultNotSetNetworkSettings, from: self) errorNotifier.notify(PacketTunnelProviderError.coultNotSetNetworkSettings)
startTunnelCompletionHandler(PacketTunnelProviderError.coultNotSetNetworkSettings) startTunnelCompletionHandler(PacketTunnelProviderError.coultNotSetNetworkSettings)
} else { } else {
startTunnelCompletionHandler(nil /* No errors */) startTunnelCompletionHandler(nil /* No errors */)
@ -128,6 +130,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
networkMonitor?.cancel() networkMonitor?.cancel()
networkMonitor = nil networkMonitor = nil
ErrorNotifier.removeLastErrorFile()
wg_log(.info, staticMessage: "Stopping tunnel") wg_log(.info, staticMessage: "Stopping tunnel")
if let handle = wgHandle { if let handle = wgHandle {
wgTurnOff(handle) wgTurnOff(handle)