Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate VPN errors before re-throwing them #1054

Merged
merged 8 commits into from
Nov 3, 2024
68 changes: 66 additions & 2 deletions Sources/NetworkProtection/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
case rekeyAttempt(_ step: RekeyAttemptStep)
case failureRecoveryAttempt(_ step: FailureRecoveryStep)
case serverMigrationAttempt(_ step: ServerMigrationAttemptStep)
case malformedErrorDetected(_ error: Error)
}

public enum AttemptStep: CustomDebugStringConvertible {
Expand Down Expand Up @@ -710,7 +711,13 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
Logger.networkProtection.log("🔴 Stopping VPN due to no auth token")
await attemptShutdownDueToRevokedAccess()

throw error
// Check that the error is valid and able to be re-thrown to the OS before shutting the tunnel down
if let wrappedError = wrapped(error: error) {
providerEvents.fire(.malformedErrorDetected(error))
throw wrappedError
} else {
throw error
}
}

do {
Expand All @@ -737,7 +744,14 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
self.knownFailureStore.lastKnownFailure = KnownFailure(error)

providerEvents.fire(.tunnelStartAttempt(.failure(error)))
throw error

// Check that the error is valid and able to be re-thrown to the OS before shutting the tunnel down
if let wrappedError = wrapped(error: error) {
providerEvents.fire(.malformedErrorDetected(error))
throw wrappedError
} else {
throw error
}
}
}

Expand Down Expand Up @@ -1815,6 +1829,56 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
snoozeTimingStore.reset()
}

// MARK: - Error Validation

enum InvalidDiagnosticError: Error, CustomNSError {
case errorWithInvalidUnderlyingError(Error)

var errorCode: Int {
switch self {
case .errorWithInvalidUnderlyingError(let error):
return (error as NSError).code
}
}

var localizedDescription: String {
switch self {
case .errorWithInvalidUnderlyingError(let error):
return "Error '\(type(of: error))', message: \(error.localizedDescription)"
}
}

var errorUserInfo: [String: Any] {
switch self {
case .errorWithInvalidUnderlyingError(let error):
let newError = NSError(domain: (error as NSError).domain, code: (error as NSError).code)
return [NSUnderlyingErrorKey: newError]
Comment on lines +1854 to +1855
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to know what error violated the underlying error validation, but without actually including its underlying errors - knowing its domain and code should be enough to debug further.

}
}
}

/// Wraps an error instance in a new error type in cases where it is malformed; i.e., doesn't use an `NSError` instance for its underlying error, etc.
private func wrapped(error: Error) -> Error? {
if containsValidUnderlyingError(error) {
return nil
} else {
return InvalidDiagnosticError.errorWithInvalidUnderlyingError(error)
}
}

private func containsValidUnderlyingError(_ error: Error) -> Bool {
let nsError = error as NSError

if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? Error {
return containsValidUnderlyingError(underlyingError)
} else if nsError.userInfo[NSUnderlyingErrorKey] != nil {
// If `NSUnderlyingErrorKey` exists but is not an `Error`, return false
return false
}

return true
}

}

extension WireGuardAdapterError: LocalizedError, CustomDebugStringConvertible {
Expand Down
Loading