diff --git a/Sources/NetworkProtection/Diagnostics/NetworkProtectionError.swift b/Sources/NetworkProtection/Diagnostics/NetworkProtectionError.swift index 9f17fa922..6e2a3da4c 100644 --- a/Sources/NetworkProtection/Diagnostics/NetworkProtectionError.swift +++ b/Sources/NetworkProtection/Diagnostics/NetworkProtectionError.swift @@ -66,6 +66,9 @@ public enum NetworkProtectionError: LocalizedError, CustomNSError { // Subscription errors case vpnAccessRevoked + // Login item errors + case loginItemVersionMismatched + // Unhandled error case unhandledError(function: String, line: Int, error: Error) @@ -94,6 +97,7 @@ public enum NetworkProtectionError: LocalizedError, CustomNSError { case .failedToParseRedeemResponse: return 111 case .invalidAuthToken: return 112 case .serverListInconsistency: return 113 + case .loginItemVersionMismatched: return 114 // 200+ - Keychain errors case .failedToCastKeychainValueToData: return 300 case .keychainReadError: return 201 @@ -138,7 +142,8 @@ public enum NetworkProtectionError: LocalizedError, CustomNSError { .wireGuardDnsResolution, .startWireGuardBackend, .noAuthTokenFound, - .vpnAccessRevoked: + .vpnAccessRevoked, + .loginItemVersionMismatched: return [:] case .failedToFetchServerList(let error), .failedToFetchRegisteredServers(let error), diff --git a/Sources/NetworkProtection/ExtensionMessage/ExtensionRequest.swift b/Sources/NetworkProtection/ExtensionMessage/ExtensionRequest.swift index e4f95b4b1..dad851aa7 100644 --- a/Sources/NetworkProtection/ExtensionMessage/ExtensionRequest.swift +++ b/Sources/NetworkProtection/ExtensionMessage/ExtensionRequest.swift @@ -23,6 +23,7 @@ public enum DebugCommand: Codable { case removeSystemExtension case removeVPNConfiguration case sendTestNotification + case simulateKnownFailure case disableConnectOnDemandAndShutDown } diff --git a/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift b/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift index 2a86436a2..91de28df2 100644 --- a/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift +++ b/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift @@ -41,6 +41,7 @@ public enum NetworkProtectionClientError: CustomNSError, NetworkProtectionErrorC case failedToParseRedeemResponse(Error) case invalidAuthToken case accessDenied + case loginItemVersionMismatched var networkProtectionError: NetworkProtectionError { switch self { @@ -58,6 +59,7 @@ public enum NetworkProtectionClientError: CustomNSError, NetworkProtectionErrorC case .failedToParseRedeemResponse(let error): return .failedToParseRedeemResponse(error) case .invalidAuthToken: return .invalidAuthToken case .accessDenied: return .vpnAccessRevoked + case .loginItemVersionMismatched: return .loginItemVersionMismatched } } @@ -77,6 +79,7 @@ public enum NetworkProtectionClientError: CustomNSError, NetworkProtectionErrorC case .failedToParseRedeemResponse: return 11 case .invalidAuthToken: return 12 case .accessDenied: return 13 + case .loginItemVersionMismatched: return 14 } } @@ -96,7 +99,8 @@ public enum NetworkProtectionClientError: CustomNSError, NetworkProtectionErrorC .invalidInviteCode, .failedToRetrieveAuthToken, .invalidAuthToken, - .accessDenied: + .accessDenied, + .loginItemVersionMismatched: return [:] } } diff --git a/Sources/NetworkProtection/Notifications/NetworkProtectionNotification.swift b/Sources/NetworkProtection/Notifications/NetworkProtectionNotification.swift index 829296ff5..86dedc1cc 100644 --- a/Sources/NetworkProtection/Notifications/NetworkProtectionNotification.swift +++ b/Sources/NetworkProtection/Notifications/NetworkProtectionNotification.swift @@ -116,6 +116,7 @@ public enum NetworkProtectionNotification: String { // Error Events case tunnelErrorChanged case controllerErrorChanged + case knownFailureUpdated // New Status Observer case requestStatusUpdate diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index 3c3c12ecc..7fc3f0af2 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -1021,6 +1021,8 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { handleExpireRegistrationKey(completionHandler: completionHandler) case .sendTestNotification: handleSendTestNotification(completionHandler: completionHandler) + case .simulateKnownFailure: + completionHandler?(nil) case .disableConnectOnDemandAndShutDown: Task { [weak self] in await self?.attemptShutdown { diff --git a/Sources/NetworkProtection/Status/KnownFailure.swift b/Sources/NetworkProtection/Status/KnownFailure.swift new file mode 100644 index 000000000..0069a5547 --- /dev/null +++ b/Sources/NetworkProtection/Status/KnownFailure.swift @@ -0,0 +1,38 @@ +// +// 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 + +@objc +final public class KnownFailure: NSObject, Codable { + public let domain: String + public let code: Int + public let localizedDescription: String + + public init?(_ error: Error?) { + guard let nsError = error as? NSError else { return nil } + + domain = nsError.domain + code = nsError.code + localizedDescription = nsError.localizedDescription + } + + public override var description: String { + "Error domain=\(domain) code=\(code)\n\(localizedDescription)" + } +} diff --git a/Sources/NetworkProtection/Status/KnownFailureObserver/KnownFailureObserver.swift b/Sources/NetworkProtection/Status/KnownFailureObserver/KnownFailureObserver.swift new file mode 100644 index 000000000..b1ba8d686 --- /dev/null +++ b/Sources/NetworkProtection/Status/KnownFailureObserver/KnownFailureObserver.swift @@ -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 { get } + var recentValue: KnownFailure? { get } +} diff --git a/Sources/NetworkProtection/Status/KnownFailureObserver/KnownFailureObserverThroughDistributedNotifications.swift b/Sources/NetworkProtection/Status/KnownFailureObserver/KnownFailureObserverThroughDistributedNotifications.swift new file mode 100644 index 000000000..40b84ca92 --- /dev/null +++ b/Sources/NetworkProtection/Status/KnownFailureObserver/KnownFailureObserverThroughDistributedNotifications.swift @@ -0,0 +1,56 @@ +// +// 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 +import NotificationCenter + +public class KnownFailureObserverThroughDistributedNotifications: KnownFailureObserver { + public lazy var publisher = subject.eraseToAnyPublisher() + public var recentValue: KnownFailure? { + subject.value + } + + private let subject = CurrentValueSubject(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) + } + } +} diff --git a/Sources/NetworkProtection/Status/NetworkProtectionStatusReporter.swift b/Sources/NetworkProtection/Status/NetworkProtectionStatusReporter.swift index a83e09891..f030141a4 100644 --- a/Sources/NetworkProtection/Status/NetworkProtectionStatusReporter.swift +++ b/Sources/NetworkProtection/Status/NetworkProtectionStatusReporter.swift @@ -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() } @@ -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 @@ -79,6 +81,7 @@ public final class DefaultNetworkProtectionStatusReporter: NetworkProtectionStat connectivityIssuesObserver: ConnectivityIssueObserver, controllerErrorMessageObserver: ControllerErrorMesssageObserver, dataVolumeObserver: DataVolumeObserver, + knownFailureObserver: KnownFailureObserver, distributedNotificationCenter: DistributedNotificationCenter = .default()) { self.statusObserver = statusObserver @@ -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()