Skip to content

Commit

Permalink
Surface specific XPC & login item errors (#819)
Browse files Browse the repository at this point in the history
  • Loading branch information
quanganhdo authored Jun 4, 2024
1 parent 03e6b71 commit 79fe0c9
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 4 deletions.
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 {
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

0 comments on commit 79fe0c9

Please sign in to comment.