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

Surface specific XPC & login item errors #819

Merged
merged 22 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public enum NetworkProtectionNotification: String {
// Error Events
case tunnelErrorChanged
case controllerErrorChanged
case knownFailureUpdated

// New Status Observer
case requestStatusUpdate
Expand Down
22 changes: 18 additions & 4 deletions Sources/NetworkProtection/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {

// MARK: - Error Handling

enum TunnelError: LocalizedError, CustomNSError {
public enum TunnelError: LocalizedError, CustomNSError, SilentErrorConvertible {
// Tunnel Setup Errors - 0+
case startingTunnelWithoutAuthToken
case couldNotGenerateTunnelConfiguration(internalError: Error)
Expand All @@ -72,7 +72,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
// Subscription Errors - 100+
case vpnAccessRevoked

var errorDescription: String? {
public var errorDescription: String? {
switch self {
case .startingTunnelWithoutAuthToken:
return "Missing auth token at startup"
Expand All @@ -85,7 +85,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
}
}

var errorCode: Int {
public var errorCode: Int {
switch self {
// Tunnel Setup Errors - 0+
case .startingTunnelWithoutAuthToken: return 0
Expand All @@ -96,7 +96,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
}
}

var errorUserInfo: [String: Any] {
public var errorUserInfo: [String: Any] {
switch self {
case .startingTunnelWithoutAuthToken,
.simulateTunnelFailureError,
Expand All @@ -106,6 +106,16 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
return [NSUnderlyingErrorKey: underlyingError]
}
}

public var asSilentError: KnownFailure.SilentError? {
guard case .couldNotGenerateTunnelConfiguration(let internalError) = self,
let clientError = internalError as? NetworkProtectionClientError,
case .failedToFetchRegisteredServers = clientError else {
return nil
}

return .registeredServerFetchingFailed
}
}

// MARK: - WireGuard
Expand Down Expand Up @@ -335,6 +345,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
private let bandwidthAnalyzer = NetworkProtectionConnectionBandwidthAnalyzer()
private let tunnelHealth: NetworkProtectionTunnelHealthStore
private let controllerErrorStore: NetworkProtectionTunnelErrorStore
private let knownFailureStore: NetworkProtectionKnownFailureStore

// MARK: - Cancellables

Expand All @@ -352,6 +363,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
public init(notificationsPresenter: NetworkProtectionNotificationsPresenter,
tunnelHealthStore: NetworkProtectionTunnelHealthStore,
controllerErrorStore: NetworkProtectionTunnelErrorStore,
knownFailureStore: NetworkProtectionKnownFailureStore = NetworkProtectionKnownFailureStore(),
keychainType: KeychainType,
tokenStore: NetworkProtectionTokenStore,
debugEvents: EventMapping<NetworkProtectionError>?,
Expand All @@ -369,6 +381,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
self.providerEvents = providerEvents
self.tunnelHealth = tunnelHealthStore
self.controllerErrorStore = controllerErrorStore
self.knownFailureStore = knownFailureStore
self.settings = settings
self.defaults = defaults
self.isSubscriptionEnabled = isSubscriptionEnabled
Expand Down Expand Up @@ -563,6 +576,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
os_log("Tunnel startup error: %{public}@", type: .error, errorDescription)
self?.controllerErrorStore.lastErrorMessage = errorDescription
self?.connectionStatus = .disconnected
self?.knownFailureStore.lastKnownFailure = KnownFailure(error)

providerEvents.fire(.tunnelStartAttempt(.failure(error)))
completionHandler(error)
Expand Down
50 changes: 50 additions & 0 deletions Sources/NetworkProtection/Status/KnownFailure.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// KnownFailure.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

public protocol SilentErrorConvertible: Error {
var asSilentError: KnownFailure.SilentError? { get }
}

@objc
final public class KnownFailure: NSObject, Codable {
Copy link
Contributor

Choose a reason for hiding this comment

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

This forces all errors that are "known" to be defined in BSK which seems unnecessarily restrictive.

Why instead not just make the error adhere to Codable and store that in NetworkProtectionKnownFailureStore.

Then the VPN app can decide what errors are worth passing over IPC, but at least we don't have to add errors that are macOS specific to BSK.

Copy link
Member Author

Choose a reason for hiding this comment

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

Why instead not just make the error adhere to Codable and store that in NetworkProtectionKnownFailureStore.

It's tricky as we need a concrete type to decode the error; and that type needs to be passed around in different ways (e.g. as part of XPCClientInterface, distributed notification object).

To keep it simple, I've moved the custom handling code for TunnelError outside of the initializer and instead made it so that an error conforming to InternalErrorWrapping can expose its internal error. That way we can keep specific errors outside of BSK.

Copy link
Contributor

Choose a reason for hiding this comment

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

Understood.

That said, I do think that instead of encoding domain, code and localizedDescription, we should turn this into a Codable enum. See this comment for context.

public typealias SilentErrorCode = Int

public enum SilentError: SilentErrorCode {
case operationNotPermitted
case loginItemVersionMismatched
case registeredServerFetchingFailed
}

public let error: SilentErrorCode

public init?(_ error: Error?) {
if let nsError = error as? NSError, nsError.domain == "SMAppServiceErrorDomain", nsError.code == 1 {
self.error = SilentError.operationNotPermitted.rawValue
return
}

if let error = error as? SilentErrorConvertible, let silentError = error.asSilentError {
self.error = silentError.rawValue
return
}

return nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// KnownFailureObserver.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Combine
import Foundation
import NetworkExtension

public protocol KnownFailureObserver {
var publisher: AnyPublisher<KnownFailure?, Never> { get }
var recentValue: KnownFailure? { get }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// KnownFailureObserverThroughDistributedNotifications.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#if os(macOS)

import Combine
import Foundation
import NetworkExtension
import NotificationCenter

public class KnownFailureObserverThroughDistributedNotifications: KnownFailureObserver {
public lazy var publisher = subject.eraseToAnyPublisher()
public var recentValue: KnownFailure? {
subject.value
}

private let subject = CurrentValueSubject<KnownFailure?, Never>(nil)

private let distributedNotificationCenter: DistributedNotificationCenter
private var cancellable: AnyCancellable?

public init(distributedNotificationCenter: DistributedNotificationCenter = .default()) {
self.distributedNotificationCenter = distributedNotificationCenter

start()
}

private func start() {
cancellable = distributedNotificationCenter.publisher(for: .knownFailureUpdated).sink { [weak self] notification in
self?.handleKnownFailureUpdated(notification)
}
}

private func handleKnownFailureUpdated(_ notification: Notification) {
if let object = notification.object as? String,
let data = object.data(using: .utf8),
let failure = try? JSONDecoder().decode(KnownFailure.self, from: data) {
subject.send(failure)
} else {
subject.send(nil)
}
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public protocol NetworkProtectionStatusReporter {
var connectivityIssuesObserver: ConnectivityIssueObserver { get }
var controllerErrorMessageObserver: ControllerErrorMesssageObserver { get }
var dataVolumeObserver: DataVolumeObserver { get }
var knownFailureObserver: KnownFailureObserver { get }

func forceRefresh()
}
Expand Down Expand Up @@ -70,6 +71,7 @@ public final class DefaultNetworkProtectionStatusReporter: NetworkProtectionStat
public let connectivityIssuesObserver: ConnectivityIssueObserver
public let controllerErrorMessageObserver: ControllerErrorMesssageObserver
public let dataVolumeObserver: DataVolumeObserver
public let knownFailureObserver: KnownFailureObserver

// MARK: - Init & deinit

Expand All @@ -79,6 +81,7 @@ public final class DefaultNetworkProtectionStatusReporter: NetworkProtectionStat
connectivityIssuesObserver: ConnectivityIssueObserver,
controllerErrorMessageObserver: ControllerErrorMesssageObserver,
dataVolumeObserver: DataVolumeObserver,
knownFailureObserver: KnownFailureObserver,
distributedNotificationCenter: DistributedNotificationCenter = .default()) {

self.statusObserver = statusObserver
Expand All @@ -87,6 +90,7 @@ public final class DefaultNetworkProtectionStatusReporter: NetworkProtectionStat
self.connectivityIssuesObserver = connectivityIssuesObserver
self.controllerErrorMessageObserver = controllerErrorMessageObserver
self.dataVolumeObserver = dataVolumeObserver
self.knownFailureObserver = knownFailureObserver
self.distributedNotificationCenter = distributedNotificationCenter

start()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// NetworkProtectionKnownFailureStore.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import Common

final public class NetworkProtectionKnownFailureStore {
private static let lastKnownFailureKey = "com.duckduckgo.NetworkProtectionKnownFailureStore.knownFailure"
private let userDefaults: UserDefaults

#if os(macOS)
private let distributedNotificationCenter: DistributedNotificationCenter

public init(userDefaults: UserDefaults = .standard,
distributedNotificationCenter: DistributedNotificationCenter = .default()) {
self.userDefaults = userDefaults
self.distributedNotificationCenter = distributedNotificationCenter
}
#else
public init(userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
}
#endif

public var lastKnownFailure: KnownFailure? {
get {
guard let data = userDefaults.data(forKey: Self.lastKnownFailureKey) else { return nil }
return try? JSONDecoder().decode(KnownFailure.self, from: data)
}

set {
if let newValue, let data = try? JSONEncoder().encode(newValue) {
userDefaults.set(data, forKey: Self.lastKnownFailureKey)
#if os(macOS)
postKnownFailureUpdatedNotification(data: data)
#endif
} else {
userDefaults.removeObject(forKey: Self.lastKnownFailureKey)
#if os(macOS)
postKnownFailureUpdatedNotification()
#endif
}
}
}

public func reset() {
lastKnownFailure = nil
}

// MARK: - Posting Notifications

#if os(macOS)
private func postKnownFailureUpdatedNotification(data: Data? = nil) {
let object: String? = {
guard let data else { return nil }
return String(data: data, encoding: .utf8)
}()
distributedNotificationCenter.post(.knownFailureUpdated, object: object)
}
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// MockKnownFailureObserver.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Combine
import Foundation
import NetworkProtection

public final class MockKnownFailureObserver: KnownFailureObserver {
public init() {}
public let subject = CurrentValueSubject<KnownFailure?, Never>(nil)
public lazy var publisher = subject.eraseToAnyPublisher()
public var recentValue: KnownFailure? {
subject.value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public final class MockNetworkProtectionStatusReporter: NetworkProtectionStatusR
public let connectivityIssuesObserver: ConnectivityIssueObserver
public let controllerErrorMessageObserver: ControllerErrorMesssageObserver
public let dataVolumeObserver: DataVolumeObserver
public let knownFailureObserver: KnownFailureObserver

// MARK: - Init & deinit

Expand All @@ -44,13 +45,15 @@ public final class MockNetworkProtectionStatusReporter: NetworkProtectionStatusR
connectivityIssuesObserver: ConnectivityIssueObserver = MockConnectivityIssueObserver(),
controllerErrorMessageObserver: ControllerErrorMesssageObserver = MockControllerErrorMesssageObserver(),
dataVolumeObserver: DataVolumeObserver = MockDataVolumeObserver(),
knownFailureObserver: KnownFailureObserver = MockKnownFailureObserver(),
distributedNotificationCenter: DistributedNotificationCenter = .default()) {

self.statusObserver = statusObserver
self.serverInfoObserver = serverInfoObserver
self.connectionErrorObserver = connectionErrorObserver
self.connectivityIssuesObserver = connectivityIssuesObserver
self.dataVolumeObserver = dataVolumeObserver
self.knownFailureObserver = knownFailureObserver
self.controllerErrorMessageObserver = controllerErrorMessageObserver
}

Expand Down
Loading
Loading