From d5c1756e2264d08cf308f79abe8fd22d126fccd1 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 21 Feb 2024 21:54:34 +0100 Subject: [PATCH] macOS: Transparent proxy for excluding VPN traffic. (#2128) Task/Issue URL: https://app.asana.com/0/0/1206462407536023/f Tech Design URLs: - [Tech Design: How to exclude Data Broker traffic?](https://app.asana.com/0/481882893211075/1206363506060150/f) - [Tech Design: Mechanism to allow PIR to start excluding its traffic from the VPN tunnel](https://app.asana.com/0/481882893211075/1206446978081253/f) - [Tech Design: How will the proxy recover from failure?](https://app.asana.com/0/481882893211075/1206446978546262) iOS PR: https://github.com/duckduckgo/iOS/pull/2429 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/652 ## Description Adds a transparent proxy that allows excluding app and domain traffic from the VPN. ## Known issues / limitations ### Issue 1: Exclusion delay on existing flows When switching off an exclusion, connection flows seem to switch immediately to routing through the tunnel interface again. However when turning the exclusion back ON, connection flows seem to take a bit before routing back through the proxy. This should not be a big problem as eventually connections start being excluded correctly again. It's unclear at this point if this is a macOS bug, or a bug on our proxy - but I don't think this should be a blocker by any means. --- Configuration/App/DuckDuckGoAppStore.xcconfig | 5 - Configuration/AppStore.xcconfig | 26 +- Configuration/DeveloperID.xcconfig | 14 + .../NetworkProtectionAppExtension.xcconfig | 40 +- .../VPNProxyExtension.xcconfig | 52 +++ DuckDuckGo.xcodeproj/project.pbxproj | 287 +++++++++++-- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../DBP/DataBrokerProtectionDebugMenu.swift | 10 + .../DBP/DataBrokerProtectionManager.swift | 10 +- .../DBP/LoginItem+DataBrokerProtection.swift | 1 + DuckDuckGo/DuckDuckGo.entitlements | 1 + DuckDuckGo/DuckDuckGoAppStore.entitlements | 5 + DuckDuckGo/DuckDuckGoAppStoreCI.entitlements | 4 - DuckDuckGo/DuckDuckGoDebug.entitlements | 1 + DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements | 30 -- .../DuckDuckGo_NetP_Release.entitlements | 38 -- DuckDuckGo/InfoPlist.xcstrings | 2 +- .../Bundle+VPN.swift | 65 +++ .../NetworkProtectionBundle.swift | 78 ---- .../NetworkProtectionDebugMenu.swift | 46 ++- .../NetworkProtectionTunnelController.swift | 19 +- .../MacPacketTunnelProvider.swift | 20 +- .../MacTransparentProxyProvider.swift | 94 +++++ ...NetworkProtectionAppExtension.entitlements | 1 + .../BrowserWindowManager.swift | 64 +++ .../IPCServiceManager.swift | 13 +- DuckDuckGoVPN/DuckDuckGoVPN.entitlements | 1 + DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 93 ++++- DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements | 1 + DuckDuckGoVPN/Info-AppStore.plist | 6 +- DuckDuckGoVPN/Info.plist | 6 +- DuckDuckGoVPN/VPNProxyLauncher.swift | 149 +++++++ .../DataBrokerProtection/Package.swift | 2 +- .../IPC/DataBrokerProtectionIPCClient.swift | 13 +- .../IPC/DataBrokerProtectionIPCServer.swift | 16 + .../Pixels/DataBrokerProtectionPixels.swift | 1 - LocalPackages/LoginItems/Package.swift | 2 +- .../NetworkProtectionMac/Package.resolved | 104 +++++ .../NetworkProtectionMac/Package.swift | 16 +- .../FlowManagers/TCPFlowManager.swift | 242 +++++++++++ .../FlowManagers/UDPFlowManager.swift | 329 +++++++++++++++ .../TransparentProxyAppMessageHandler.swift | 82 ++++ .../IPC/TransparentProxyRequest.swift | 67 +++ .../TransparentProxyControllerPixel.swift | 89 ++++ .../TransparentProxyProviderPixel.swift | 93 +++++ .../RoutingRules/VPNAppRoutingRules.swift | 16 +- .../RoutingRules/VPNRoutingRule.swift | 20 +- .../Settings/TransparentProxySettings.swift | 134 ++++++ .../UserDefaults+excludedApps.swift | 79 ++++ .../UserDefaults+excludedDomains.swift | 51 +++ .../TransparentProxyController.swift | 293 +++++++++++++ .../TransparentProxyProvider.swift | 389 ++++++++++++++++++ ...ransparentProxyProviderConfiguration.swift | 40 ++ .../NetworkProtectionStatusView.swift | 4 + .../NetworkProtectionStatusViewModel.swift | 34 +- ...TransparentProxyControllerPixelTests.swift | 120 ++++++ .../TransparentProxyProviderPixelTests.swift | 66 +++ LocalPackages/PixelKit/Package.swift | 2 +- .../PixelKit/PixelKit+Parameters.swift | 6 +- .../PixelKit/Sources/PixelKit/PixelKit.swift | 24 +- .../Sources/PixelKit/PixelKitEvent.swift | 2 +- .../Sources/PixelKit/PixelKitEventV2.swift | 70 ++++ .../PixelFireExpectations.swift | 36 ++ .../XCTestCase+PixelKit.swift | 148 +++++++ LocalPackages/SubscriptionUI/Package.swift | 2 +- LocalPackages/SwiftUIExtensions/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- LocalPackages/XPCHelper/Package.swift | 2 +- NetworkProtectionSystemExtension/Info.plist | 2 + ...otectionSystemExtension_Debug.entitlements | 1 + ...ectionSystemExtension_Release.entitlements | 1 + VPNProxyExtension/Info.plist | 17 + .../VPNProxyExtension.entitlements | 25 ++ fastlane/Matchfile | 2 + scripts/assets/AppStoreExportOptions.plist | 4 + 76 files changed, 3497 insertions(+), 341 deletions(-) create mode 100644 Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig delete mode 100644 DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements delete mode 100644 DuckDuckGo/DuckDuckGo_NetP_Release.entitlements create mode 100644 DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift delete mode 100644 DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift create mode 100644 DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift create mode 100644 DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift create mode 100644 DuckDuckGoVPN/VPNProxyLauncher.swift create mode 100644 LocalPackages/NetworkProtectionMac/Package.resolved create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift rename DuckDuckGoVPN/Bundle+Configuration.swift => LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift (56%) rename DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift => LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift (59%) create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift create mode 100644 LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift create mode 100644 LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift create mode 100644 LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift create mode 100644 LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift create mode 100644 LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift create mode 100644 VPNProxyExtension/Info.plist create mode 100644 VPNProxyExtension/VPNProxyExtension.entitlements diff --git a/Configuration/App/DuckDuckGoAppStore.xcconfig b/Configuration/App/DuckDuckGoAppStore.xcconfig index 3ee212ad5e..904caca8c5 100644 --- a/Configuration/App/DuckDuckGoAppStore.xcconfig +++ b/Configuration/App/DuckDuckGoAppStore.xcconfig @@ -17,11 +17,6 @@ #include "../AppStore.xcconfig" #include "ManualAppStoreRelease.xcconfig" -AGENT_BUNDLE_ID[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -AGENT_BUNDLE_ID[config=Debug][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug -AGENT_BUNDLE_ID[config=CI][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug -AGENT_BUNDLE_ID[config=Review][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.review - PRODUCT_BUNDLE_IDENTIFIER = $(MAIN_BUNDLE_IDENTIFIER) CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAppStore.entitlements diff --git a/Configuration/AppStore.xcconfig b/Configuration/AppStore.xcconfig index c2ae87c9b5..0ad3f9f6b5 100644 --- a/Configuration/AppStore.xcconfig +++ b/Configuration/AppStore.xcconfig @@ -50,21 +50,23 @@ AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review AGENT_PRODUCT_NAME = DuckDuckGo VPN App Store AGENT_RELEASE_PRODUCT_NAME = DuckDuckGo VPN -SYSEX_BUNDLE_ID[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -SYSEX_BUNDLE_ID[config=Debug][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug.network-protection-extension -SYSEX_BUNDLE_ID[config=CI][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug.network-protection-extension -SYSEX_BUNDLE_ID[config=Review][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension -SYSEX_BUNDLE_ID[config=Release][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension +// Extensions -// Distributed Notifications Prefix +PROXY_EXTENSION_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(AGENT_BUNDLE_ID).proxy + +TUNNEL_EXTENSION_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension -SYSEX_BUNDLE_ID_BASE[sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Release][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension +// Distributed Notifications Prefix -DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(SYSEX_BUNDLE_ID_BASE) +DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(AGENT_BUNDLE_ID_BASE).network-extension DISTRIBUTED_NOTIFICATIONS_PREFIX[config=CI][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).ci DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Review][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).review diff --git a/Configuration/DeveloperID.xcconfig b/Configuration/DeveloperID.xcconfig index 0bfb9bb8cb..b66acc76d2 100644 --- a/Configuration/DeveloperID.xcconfig +++ b/Configuration/DeveloperID.xcconfig @@ -65,6 +65,20 @@ AGENT_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review AGENT_PRODUCT_NAME = DuckDuckGo VPN +// Extensions + +PROXY_EXTENSION_BUNDLE_ID[sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(SYSEX_BUNDLE_ID) + +TUNNEL_EXTENSION_BUNDLE_ID[sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(SYSEX_BUNDLE_ID) + // DBP DBP_BACKGROUND_AGENT_PRODUCT_NAME = DuckDuckGo Personal Information Removal diff --git a/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig b/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig index 60ad407569..2fb095fc56 100644 --- a/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig +++ b/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig @@ -14,10 +14,7 @@ // #include "../ExtensionBase.xcconfig" - -// Since we're using nonstandard bundle IDs we'll just define them here, but we should consider -// standardizing the bundle IDs so we can just define BUNDLE_IDENTIFIER_PREFIX -BUNDLE_IDENTIFIER_PREFIX = com.duckduckgo.mobile.ios.vpn.agent +#include "../../AppStore.xcconfig" CODE_SIGN_ENTITLEMENTS[config=CI][sdk=macosx*] = CODE_SIGN_ENTITLEMENTS[config=Debug][sdk=macosx*] = DuckDuckGo/NetworkProtectionAppExtension.entitlements @@ -38,17 +35,11 @@ FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION -NETP_BASE_APP_GROUP = $(DEVELOPMENT_TEAM).com.duckduckgo.macos.browser.network-protection -NETP_APP_GROUP[config=CI][sdk=macos*] = $(NETP_BASE_APP_GROUP).debug -NETP_APP_GROUP[config=Review][sdk=macos*] = $(NETP_BASE_APP_GROUP).review -NETP_APP_GROUP[config=Debug][sdk=macos*] = $(NETP_BASE_APP_GROUP).debug -NETP_APP_GROUP[config=Release][sdk=macos*] = $(NETP_BASE_APP_GROUP) - PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = -PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).debug.network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).debug.network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).review.network-protection-extension +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) PROVISIONING_PROFILE_SPECIFIER[config=CI][sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = match AppStore com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension macos @@ -59,24 +50,3 @@ SKIP_INSTALL = YES SWIFT_EMIT_LOC_STRINGS = YES LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks @executable_path/../../../../Frameworks - -// Distributed Notifications: - -AGENT_BUNDLE_ID_BASE[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -AGENT_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID_BASE) -AGENT_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug -AGENT_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug -AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review - -SYSEX_BUNDLE_ID_BASE[sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Release][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension - -DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(SYSEX_BUNDLE_ID_BASE) - -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=CI][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).ci -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Review][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).review -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Debug][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).debug -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Release][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE) diff --git a/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig b/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig new file mode 100644 index 0000000000..5f70d87091 --- /dev/null +++ b/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig @@ -0,0 +1,52 @@ +// Copyright © 2023 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. +// + +#include "../ExtensionBase.xcconfig" +#include "../../AppStore.xcconfig" + +CODE_SIGN_ENTITLEMENTS[config=CI][sdk=macosx*] = +CODE_SIGN_ENTITLEMENTS[config=Debug][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_ENTITLEMENTS[config=Release][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_ENTITLEMENTS[config=Review][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_STYLE[config=Debug][sdk=*] = Automatic + +CODE_SIGN_IDENTITY[sdk=macosx*] = 3rd Party Mac Developer Application +CODE_SIGN_IDENTITY[config=Debug][sdk=macosx*] = Apple Development +CODE_SIGN_IDENTITY[config=CI][sdk=macosx*] = + +GENERATE_INFOPLIST_FILE = YES +INFOPLIST_FILE = VPNProxyExtension/Info.plist +INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023 DuckDuckGo. All rights reserved. + +FEATURE_FLAGS[arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION + +PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) + +PROVISIONING_PROFILE_SPECIFIER[config=CI][sdk=macosx*] = +PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = match AppStore $(AGENT_BUNDLE_ID).proxy macos +PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = match AppStore $(AGENT_BUNDLE_ID).proxy macos + +SDKROOT = macosx +SKIP_INSTALL = YES +SWIFT_EMIT_LOC_STRINGS = YES + +LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks @executable_path/../../../../Frameworks diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0f1e8a2d3e..0a586a3376 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1099,12 +1099,11 @@ 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B2537722A11BF8B00610219 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B25376F2A11BF8B00610219 /* main.swift */; }; 4B2537772A11BFE100610219 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2537762A11BFE100610219 /* PixelKit */; }; - 4B2537782A11C00F00610219 /* NetworkProtectionExtensionMachService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */; }; 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B29759728281F0900187C4E /* FirefoxEncryptionKeyReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B29759628281F0900187C4E /* FirefoxEncryptionKeyReader.swift */; }; 4B2975992828285900187C4E /* FirefoxKeyReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2975982828285900187C4E /* FirefoxKeyReaderTests.swift */; }; 4B2AAAF529E70DEA0026AFC0 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2AAAF429E70DEA0026AFC0 /* Lottie */; }; - 4B2D06292A11C0C900DE1F49 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2D062B2A11C0E100DE1F49 /* Networking */; }; 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; @@ -1168,17 +1167,16 @@ 4B44FEF52B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B44FEF22B1FEF5A000619D8 /* FocusableTextEditor.swift */; }; 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC382A11B509001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift */; }; 4B4BEC3E2A11B56E001D9AC5 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC322A11B509001D9AC5 /* Logging.swift */; }; - 4B4BEC402A11B5B5001D9AC5 /* NetworkProtectionExtensionMachService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */; }; 4B4BEC412A11B5BD001D9AC5 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */; }; 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; - 4B4BEC432A11B5C7001D9AC5 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B4BEC482A11B61F001D9AC5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B4BEC342A11B509001D9AC5 /* Assets.xcassets */; }; 4B4D603F2A0B290200BCD287 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */; }; 4B4D60982A0B2A5C00BCD287 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B4D60972A0B2A5C00BCD287 /* PixelKit */; }; 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; - 4B4D60A02A0B2D5B00BCD287 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; @@ -1202,7 +1200,7 @@ 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 4B4D60DD2A0C875E00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B4D60DF2A0C875F00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; - 4B4D60E02A0C875F00BCD287 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4D60E32A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */; }; @@ -1528,7 +1526,7 @@ 4B957A422AC7AE700062CA31 /* SafariFaviconsReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0AACAD28BC6FD0001038AC /* SafariFaviconsReader.swift */; }; 4B957A432AC7AE700062CA31 /* NSScreenExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */; }; 4B957A442AC7AE700062CA31 /* NSBezierPathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65E6B9F26D9F10600095F96 /* NSBezierPathExtension.swift */; }; - 4B957A452AC7AE700062CA31 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B957A452AC7AE700062CA31 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B957A462AC7AE700062CA31 /* WebsiteDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6820E325502F19005ED0D5 /* WebsiteDataStore.swift */; }; 4B957A472AC7AE700062CA31 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; 4B957A482AC7AE700062CA31 /* PermissionContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C852926942AC90048FEBE /* PermissionContextMenu.swift */; }; @@ -2049,7 +2047,7 @@ 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8F52402A18326600BE7131 /* NetworkProtectionTunnelController.swift */; }; 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 4BA7C4DD2B3F64E500AFE511 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 4BA7C4DC2B3F64E500AFE511 /* LoginItems */; }; - 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in Embed Network Extensions */ = {isa = PBXBuildFile; fileRef = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4BB6CE5F26B77ED000EC5860 /* Cryptography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB6CE5E26B77ED000EC5860 /* Cryptography.swift */; }; 4BB88B4525B7B55C006F6B06 /* DebugUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4425B7B55C006F6B06 /* DebugUserScript.swift */; }; 4BB88B4A25B7B690006F6B06 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; @@ -2133,7 +2131,7 @@ 4BF97AD62B43C45800EB4240 /* NetworkProtectionNavBarPopoverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3618C12ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift */; }; 4BF97AD72B43C53D00EB4240 /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; 4BF97AD82B43C5B300EB4240 /* NetworkProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */; }; - 4BF97AD92B43C5C000EB4240 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4BF97ADA2B43C5DC00EB4240 /* VPNFeedbackCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526632B1D55D80054955A /* VPNFeedbackCategory.swift */; }; 4BF97ADB2B43C5E000EB4240 /* VPNMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B05265D2B1AE5C70054955A /* VPNMetadataCollector.swift */; }; 4BF97ADC2B43C5E200EB4240 /* VPNFeedbackSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526602B1D55320054955A /* VPNFeedbackSender.swift */; }; @@ -2174,6 +2172,14 @@ 56D145F229E6F06D00E3488A /* MockBookmarkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */; }; 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; 56D6A3D729DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; + 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; + 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */; }; + 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */; }; + 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; + 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; + 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; + 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; + 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */; }; 7B1E819E27C8874900FF0E60 /* ContentOverlayPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */; }; 7B1E819F27C8874900FF0E60 /* ContentOverlay.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */; }; 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */; }; @@ -2190,15 +2196,25 @@ 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4CE8E626F02134009134B1 /* TabBarTests.swift */; }; 7B5DD69A2AE51FFA001DE99C /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B5DD6992AE51FFA001DE99C /* PixelKit */; }; 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B5F9A742AE2BE4E002AEBC0 /* PixelKit */; }; + 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; + 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7B7DFB212B7E7473009EA1A3 /* Networking */; }; 7B8C083C2AE1268E00F4C67F /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B8C083B2AE1268E00F4C67F /* PixelKit */; }; 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */; }; 7B934C412A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; + 7B94E1652B7ED95100E32B96 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */; }; + 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */; }; + 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD5A2B7E0B85004FEF43 /* Common */; }; + 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; + 7B97CD5D2B7E0BCE004FEF43 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; + 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; + 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; + 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; + 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD612B7E0C4B004FEF43 /* PixelKit */; }; + 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */; }; 7BA4727D26F01BC400EAA165 /* CoreDataTestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C42667104B00AD2C21 /* CoreDataTestUtilities.swift */; }; 7BA59C9B2AE18B49009A97B1 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */; }; 7BA7CC392AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */; }; 7BA7CC3A2AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */; }; - 7BA7CC3B2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */; }; - 7BA7CC3C2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */; }; 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */; }; 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */; }; 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */; }; @@ -2214,8 +2230,8 @@ 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606A2A0B29FA00BCD287 /* NetworkProtectionControllerErrorStore.swift */; }; 7BA7CC4E2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; 7BA7CC502AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; - 7BA7CC532AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; - 7BA7CC542AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; + 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 7BA7CC552AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; @@ -2233,9 +2249,13 @@ 7BBD44282AD730A400D0A064 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBD44272AD730A400D0A064 /* PixelKit */; }; 7BBD45B12A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBD45B22A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; + 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */; }; 7BD01C192AD8319C0088B32E /* IPCServiceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */; }; 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */; }; + 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; + 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; + 7BDA36F92B7E084A00AD5388 /* VPNProxyExtension.appex in Embed Network Extensions */ = {isa = PBXBuildFile; fileRef = 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; 7BE146082A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; 7BEC182F2AD5D8DC00D30536 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */; }; @@ -3104,7 +3124,6 @@ EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; EECE10E529DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; EECE10E629DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; - EEF12E6E2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; F41D174125CB131900472416 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; @@ -3185,6 +3204,13 @@ remoteGlobalIDString = AA585D7D248FD31100E9A3E2; remoteInfo = "DuckDuckGo Privacy Browser"; }; + 7BDA36F72B7E082100AD5388 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AA585D76248FD31100E9A3E2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7BDA36E42B7E037100AD5388; + remoteInfo = VPNProxyExtension; + }; 7BEC18302AD5DA3300D30536 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AA585D76248FD31100E9A3E2 /* Project object */; @@ -3242,14 +3268,16 @@ name = "Embed Login Items"; runOnlyForDeploymentPostprocessing = 0; }; - 4BA7C4E02B3F6F7500AFE511 /* CopyFiles */ = { + 4BA7C4E02B3F6F7500AFE511 /* Embed Network Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( - 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in CopyFiles */, + 7BDA36F92B7E084A00AD5388 /* VPNProxyExtension.appex in Embed Network Extensions */, + 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in Embed Network Extensions */, ); + name = "Embed Network Extensions"; runOnlyForDeploymentPostprocessing = 0; }; B6EC37E629B5DA2A001ACE79 /* CopyFiles */ = { @@ -3546,7 +3574,7 @@ 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetworkProtectionSystemExtension.xcconfig; sourceTree = ""; }; 4B4D60502A0B293C00BCD287 /* NetworkProtectionAppExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetworkProtectionAppExtension.xcconfig; sourceTree = ""; }; 4B4D60512A0B293C00BCD287 /* ExtensionBase.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ExtensionBase.xcconfig; sourceTree = ""; }; - 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionBundle.swift; sourceTree = ""; }; + 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+VPN.swift"; sourceTree = ""; }; 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOptionKeyExtension.swift; sourceTree = ""; }; 4B4D60652A0B29FA00BCD287 /* NetworkProtectionNavBarButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarButtonModel.swift; sourceTree = ""; }; 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+ConvenienceInitializers.swift"; sourceTree = ""; }; @@ -3556,10 +3584,7 @@ 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteCodeViewModel.swift; sourceTree = ""; }; 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventMapping+NetworkProtectionError.swift"; sourceTree = ""; }; 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationsPresenter.swift; sourceTree = ""; }; - 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionExtensionMachService.swift; sourceTree = ""; }; 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtectionExtensions.swift"; sourceTree = ""; }; - 4B4D609C2A0B2C2300BCD287 /* DuckDuckGo_NetP_Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGo_NetP_Release.entitlements; sourceTree = ""; }; - 4B4D609E2A0B2C2300BCD287 /* DuckDuckGo_NetP_Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGo_NetP_Debug.entitlements; sourceTree = ""; }; 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtection.swift"; sourceTree = ""; }; 4B4D60E12A0C883A00BCD287 /* AppMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMain.swift; sourceTree = ""; }; 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; @@ -3766,7 +3791,10 @@ 56D145ED29E6DAD900E3488A /* DataImportProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportProviderTests.swift; sourceTree = ""; }; 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBookmarkManager.swift; sourceTree = ""; }; 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueSetUpView.swift; sourceTree = ""; }; + 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowManager.swift; sourceTree = ""; }; + 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTransparentProxyProvider.swift; sourceTree = ""; }; 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; + 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNProxyLauncher.swift; sourceTree = ""; }; 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayPopover.swift; sourceTree = ""; }; 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ContentOverlay.storyboard; sourceTree = ""; }; 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayViewController.swift; sourceTree = ""; }; @@ -3790,7 +3818,6 @@ 7BA7CC0B2AD11D1E0042E5CE /* DuckDuckGoVPNAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPNAppStore.xcconfig; sourceTree = ""; }; 7BA7CC0C2AD11D1E0042E5CE /* DuckDuckGoVPN.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPN.xcconfig; sourceTree = ""; }; 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckDuckGoVPNAppDelegate.swift; sourceTree = ""; }; - 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Configuration.swift"; sourceTree = ""; }; 7BA7CC102AD11DC80042E5CE /* Info-AppStore.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-AppStore.plist"; sourceTree = ""; }; 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelControllerIPCService.swift; sourceTree = ""; }; 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -3809,6 +3836,10 @@ 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionController.swift; sourceTree = ""; }; 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainType+ClientDefault.swift"; sourceTree = ""; }; 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibility.swift; sourceTree = ""; }; + 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = VPNProxyExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7BDA36EA2B7E037200AD5388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VPNProxyExtension.entitlements; sourceTree = ""; }; + 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = VPNProxyExtension.xcconfig; sourceTree = ""; }; 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugMenu.swift; sourceTree = ""; }; 7BEC182D2AD5D89C00D30536 /* SystemExtensionManager */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SystemExtensionManager; sourceTree = ""; }; 7BEC20402B0F505F00243D3E /* AddBookmarkPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkPopoverView.swift; sourceTree = ""; }; @@ -4443,6 +4474,7 @@ 373FB4B32B4D6C4B004C88D6 /* PreferencesViews in Frameworks */, 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */, 4BF97AD32B43C43F00EB4240 /* NetworkProtectionUI in Frameworks */, + 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */, B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */, 984FD3BF299ACF35007334DD /* Bookmarks in Frameworks */, 37A5E2F0298AA1B20047046B /* Persistence in Frameworks */, @@ -4510,6 +4542,7 @@ 37269F012B332FC8005E8E46 /* Common in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, 4B2537772A11BFE100610219 /* PixelKit in Frameworks */, + 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */, 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */, 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */, ); @@ -4520,6 +4553,7 @@ buildActionMask = 2147483647; files = ( 4B41EDAB2B1544B2001EEDF4 /* LoginItems in Frameworks */, + 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */, 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */, 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */, 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */, @@ -4536,6 +4570,7 @@ 7BFCB7502ADE7E2300DA3EA7 /* PixelKit in Frameworks */, 7BA7CC612AD1211C0042E5CE /* Networking in Frameworks */, 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */, + 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */, EE7295EF2A545C12008C0991 /* NetworkProtection in Frameworks */, 4B2D067F2A1334D700DE1F49 /* NetworkProtectionUI in Frameworks */, 4BA7C4DD2B3F64E500AFE511 /* LoginItems in Frameworks */, @@ -4571,6 +4606,7 @@ 3143C8792B0D1F3D00382627 /* DataBrokerProtection in Frameworks */, 372217842B33380E00B8E9C2 /* TestUtils in Frameworks */, 4B957BD62AC7AE700062CA31 /* LoginItems in Frameworks */, + 7B94E1652B7ED95100E32B96 /* NetworkProtectionProxy in Frameworks */, 4B957BD72AC7AE700062CA31 /* NetworkProtection in Frameworks */, 4B957BD82AC7AE700062CA31 /* BrowserServicesKit in Frameworks */, 4B957BDA2AC7AE700062CA31 /* Bookmarks in Frameworks */, @@ -4613,6 +4649,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E22B7E037100AD5388 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */, + 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */, + 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */, + 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */, + 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8C62AAA39A70026E7DC /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -4661,6 +4709,7 @@ 37DF000529F9C056002B7D3E /* SyncDataProviders in Frameworks */, 37BA812D29B3CD690053F1A3 /* SyncUI in Frameworks */, 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */, + 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */, 4B4D60B12A0C83B900BCD287 /* NetworkProtectionUI in Frameworks */, 98A50964294B691800D10880 /* Persistence in Frameworks */, ); @@ -5240,8 +5289,9 @@ 4B18E32C2A1ECF1F005D0AAA /* NetworkProtection */ = { isa = PBXGroup; children = ( - 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */, 4B4D60502A0B293C00BCD287 /* NetworkProtectionAppExtension.xcconfig */, + 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */, + 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */, ); path = NetworkProtection; sourceTree = ""; @@ -5379,7 +5429,7 @@ 4B4D605D2A0B29FA00BCD287 /* AppAndExtensionAndNotificationTargets */ = { isa = PBXGroup; children = ( - 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */, + 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */, 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */, B602E8152A1E2570006D261F /* URL+NetworkProtection.swift */, ); @@ -5463,7 +5513,6 @@ isa = PBXGroup; children = ( B602E81F2A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift */, - 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */, ); path = SystemExtensionAndNotificationTargets; sourceTree = ""; @@ -5481,6 +5530,7 @@ children = ( 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */, EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, + 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */, ); path = NetworkExtensionTargets; sourceTree = ""; @@ -6126,11 +6176,11 @@ isa = PBXGroup; children = ( 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */, - 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */, 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */, 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */, 7BA7CC152AD11DC80042E5CE /* NetworkProtectionBouncer.swift */, 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */, + 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */, 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */, 7BA7CC172AD11DC80042E5CE /* UserText.swift */, 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */, @@ -6160,6 +6210,15 @@ path = LetsMove1.25; sourceTree = ""; }; + 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */ = { + isa = PBXGroup; + children = ( + 7BDA36EA2B7E037200AD5388 /* Info.plist */, + 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */, + ); + path = VPNProxyExtension; + sourceTree = ""; + }; 853014D425E6709500FB8205 /* Support */ = { isa = PBXGroup; children = ( @@ -6523,6 +6582,7 @@ 9D9AE9152AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift */, 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */, 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */, + 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */, 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */, 9D9AE9172AAA3B450026E7DC /* UserText.swift */, 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */, @@ -6646,6 +6706,7 @@ B6EC37E929B5DA2A001ACE79 /* tests-server */, 7B96D0D02ADFDA7F007E02C8 /* DuckDuckGoDBPTests */, 4B5F14F72A148B230060320F /* NetworkProtectionAppExtension */, + 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */, 4B25375C2A11BE7500610219 /* NetworkProtectionSystemExtension */, 9D9AE9132AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgent */, 7BA7CC0D2AD11DC80042E5CE /* DuckDuckGoVPN */, @@ -6676,6 +6737,7 @@ 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */, 565E46DD2B2725DC0013AC2A /* SyncE2EUITests.xctest */, 376113D42B29CD5B00E794BB /* SyncE2EUITests App Store.xctest */, + 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */, ); name = Products; sourceTree = ""; @@ -6746,8 +6808,6 @@ 4B5F15032A1570F10060320F /* DuckDuckGoDebug.entitlements */, 37D9BBA329376EE8000B99F9 /* DuckDuckGoAppStore.entitlements */, 377E54382937B7C400780A0A /* DuckDuckGoAppStoreCI.entitlements */, - 4B4D609E2A0B2C2300BCD287 /* DuckDuckGo_NetP_Debug.entitlements */, - 4B4D609C2A0B2C2300BCD287 /* DuckDuckGo_NetP_Release.entitlements */, 4B2D06642A132F3A00DE1F49 /* NetworkProtectionAppExtension.entitlements */, 4B5F14C42A145D6A0060320F /* NetworkProtectionVPNController.entitlements */, 56CEE9092B7A66C500CF10AA /* Info.plist */, @@ -8280,6 +8340,7 @@ 4BF97AD42B43C43F00EB4240 /* NetworkProtection */, 373FB4B22B4D6C4B004C88D6 /* PreferencesViews */, 312978892B64131200B67619 /* DataBrokerProtection */, + 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8395,6 +8456,7 @@ 4B2D062B2A11C0E100DE1F49 /* Networking */, EE7295E82A545BC4008C0991 /* NetworkProtection */, 37269F002B332FC8005E8E46 /* Common */, + 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -8425,6 +8487,7 @@ 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */, 7BFCB74D2ADE7E1A00DA3EA7 /* PixelKit */, 4B41EDAA2B1544B2001EEDF4 /* LoginItems */, + 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */, ); productName = DuckDuckGoAgent; productReference = 4B2D06392A11CFBB00DE1F49 /* DuckDuckGo VPN.app */; @@ -8438,11 +8501,12 @@ 4B2D06662A13318400DE1F49 /* Frameworks */, 4B2D06672A13318400DE1F49 /* Resources */, 4B2D067D2A13341200DE1F49 /* ShellScript */, - 4BA7C4E02B3F6F7500AFE511 /* CopyFiles */, + 4BA7C4E02B3F6F7500AFE511 /* Embed Network Extensions */, ); buildRules = ( ); dependencies = ( + 7BDA36F82B7E082100AD5388 /* PBXTargetDependency */, 4BA7C4DF2B3F6F4900AFE511 /* PBXTargetDependency */, B6080BA52B20AF8800B418EF /* PBXTargetDependency */, ); @@ -8453,6 +8517,7 @@ 7BA7CC602AD1211C0042E5CE /* Networking */, 7BEEA5132AD1236300A9E72B /* NetworkProtectionIPC */, 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */, + 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */, 4BA7C4DC2B3F64E500AFE511 /* LoginItems */, ); productName = DuckDuckGoAgentAppStore; @@ -8552,6 +8617,7 @@ 372217832B33380E00B8E9C2 /* TestUtils */, 373FB4B42B4D6C57004C88D6 /* PreferencesViews */, 1E21F8E22B73E48600FB272E /* Subscription */, + 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */; @@ -8596,6 +8662,29 @@ productReference = 7B4CE8DA26F02108009134B1 /* UI Tests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7BDA36F42B7E037200AD5388 /* Build configuration list for PBXNativeTarget "VPNProxyExtension" */; + buildPhases = ( + 7BDA36E12B7E037100AD5388 /* Sources */, + 7BDA36E22B7E037100AD5388 /* Frameworks */, + 7BDA36E32B7E037100AD5388 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = VPNProxyExtension; + packageProductDependencies = ( + 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */, + 7B97CD5A2B7E0B85004FEF43 /* Common */, + 7B97CD612B7E0C4B004FEF43 /* PixelKit */, + 7B7DFB212B7E7473009EA1A3 /* Networking */, + ); + productName = VPNProxyExtension; + productReference = 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 9D9AE8B22AAA39A70026E7DC /* DuckDuckGoDBPBackgroundAgent */ = { isa = PBXNativeTarget; buildConfigurationList = 9D9AE8CC2AAA39A70026E7DC /* Build configuration list for PBXNativeTarget "DuckDuckGoDBPBackgroundAgent" */; @@ -8690,6 +8779,7 @@ 37269EFA2B332F9E005E8E46 /* Common */, 3722177F2B3337FE00B8E9C2 /* TestUtils */, 373FB4B02B4D6C42004C88D6 /* PreferencesViews */, + 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -8746,7 +8836,7 @@ AA585D76248FD31100E9A3E2 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1500; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1400; ORGANIZATIONNAME = DuckDuckGo; TargetAttributes = { @@ -8788,6 +8878,9 @@ CreatedOnToolsVersion = 12.5.1; TestTargetID = AA585D7D248FD31100E9A3E2; }; + 7BDA36E42B7E037100AD5388 = { + CreatedOnToolsVersion = 15.2; + }; AA585D7D248FD31100E9A3E2 = { CreatedOnToolsVersion = 11.5; }; @@ -8833,6 +8926,7 @@ 3706FE9B293F662100E42796 /* Integration Tests App Store */, B6EC37E729B5DA2A001ACE79 /* tests-server */, 4B4D603C2A0B290200BCD287 /* NetworkProtectionAppExtension */, + 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */, 4B2537592A11BE7300610219 /* NetworkProtectionSystemExtension */, 4B4BEC1F2A11B4E2001D9AC5 /* DuckDuckGoNotifications */, 4B2D06382A11CFBA00DE1F49 /* DuckDuckGoVPN */, @@ -9050,6 +9144,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E32B7E037100AD5388 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8C92AAA39A70026E7DC /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -9861,7 +9962,7 @@ B66260E829ACD0C900E9E3EE /* DuckPlayerTabExtension.swift in Sources */, 3706FBAA293F65D500E42796 /* HoverUserScript.swift in Sources */, 3706FBAC293F65D500E42796 /* MainMenuActions.swift in Sources */, - 4BF97AD92B43C5C000EB4240 /* NetworkProtectionBundle.swift in Sources */, + 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */, 3706FBAE293F65D500E42796 /* DataImport.swift in Sources */, 3706FBAF293F65D500E42796 /* FireproofDomains.xcdatamodeld in Sources */, B626A7552991413000053070 /* SerpHeadersNavigationResponder.swift in Sources */, @@ -10520,11 +10621,11 @@ 4B2D06332A11C1E300DE1F49 /* OptionalExtension.swift in Sources */, 4BF0E50B2AD2552200FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B41EDA12B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, - 4B2537782A11C00F00610219 /* NetworkProtectionExtensionMachService.swift in Sources */, + 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */, B65DA5F32A77D3C700CBEE8D /* UserDefaultsWrapper.swift in Sources */, 4B2537722A11BF8B00610219 /* main.swift in Sources */, EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, - 4B2D06292A11C0C900DE1F49 /* NetworkProtectionBundle.swift in Sources */, + 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10543,14 +10644,14 @@ 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, B6F92BAC2A6937B3002ABA6B /* OptionalExtension.swift in Sources */, 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */, - 7BA7CC532AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */, - 7BA7CC3C2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */, + 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, 7BFE95562A9DF29B0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, + 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */, B65DA5EF2A77CC3A00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, 4BF0E5072AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, @@ -10569,10 +10670,10 @@ B6F92BA32A691583002ABA6B /* UserDefaultsWrapper.swift in Sources */, 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */, 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, + 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, 7BA7CC5A2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */, - 7BA7CC3B2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */, EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */, 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 4B0EF7292B5780EB009D6481 /* VPNAppEventsHandler.swift in Sources */, @@ -10590,7 +10691,7 @@ 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */, 7BA7CC432AD11E480042E5CE /* UserText.swift in Sources */, - 7BA7CC542AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */, + 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10601,12 +10702,11 @@ 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */, 4B4BEC3E2A11B56E001D9AC5 /* Logging.swift in Sources */, 4B4BEC412A11B5BD001D9AC5 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, - 4B4BEC432A11B5C7001D9AC5 /* NetworkProtectionBundle.swift in Sources */, + 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */, 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */, 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8222A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, B602E81A2A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, - 4B4BEC402A11B5B5001D9AC5 /* NetworkProtectionExtensionMachService.swift in Sources */, EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -10617,14 +10717,14 @@ files = ( 4B41EDA02B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, + 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, B65DA5F52A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, - 4B4D60A02A0B2D5B00BCD287 /* NetworkProtectionBundle.swift in Sources */, + 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */, 4B4D60AD2A0C807300BCD287 /* NSApplicationExtension.swift in Sources */, 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */, - EEF12E6E2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, 4BF0E50C2AD2552300FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */, B65DA5F22A77D3C600CBEE8D /* UserDefaultsWrapper.swift in Sources */, @@ -10909,7 +11009,7 @@ 4B957A422AC7AE700062CA31 /* SafariFaviconsReader.swift in Sources */, 4B957A432AC7AE700062CA31 /* NSScreenExtension.swift in Sources */, 4B957A442AC7AE700062CA31 /* NSBezierPathExtension.swift in Sources */, - 4B957A452AC7AE700062CA31 /* NetworkProtectionBundle.swift in Sources */, + 4B957A452AC7AE700062CA31 /* Bundle+VPN.swift in Sources */, B68D21CA2ACBC971002DA3C2 /* MockPrivacyConfiguration.swift in Sources */, 4B957A462AC7AE700062CA31 /* WebsiteDataStore.swift in Sources */, 4B957A472AC7AE700062CA31 /* NetworkProtectionFeatureVisibility.swift in Sources */, @@ -11378,11 +11478,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E12B7E037100AD5388 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */, + 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */, + 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */, + 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */, + 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */, + 7B97CD5D2B7E0BCE004FEF43 /* BundleExtension.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8B62AAA39A70026E7DC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, + 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, @@ -11394,6 +11508,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, @@ -11677,7 +11792,7 @@ 4B0AACAE28BC6FD0001038AC /* SafariFaviconsReader.swift in Sources */, B6B3E0E12657EA7A0040E0A2 /* NSScreenExtension.swift in Sources */, B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, - 4B4D60E02A0C875F00BCD287 /* NetworkProtectionBundle.swift in Sources */, + 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, @@ -12449,6 +12564,11 @@ target = AA585D7D248FD31100E9A3E2 /* DuckDuckGo Privacy Browser */; targetProxy = 7B4CE8DF26F02108009134B1 /* PBXContainerItemProxy */; }; + 7BDA36F82B7E082100AD5388 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */; + targetProxy = 7BDA36F72B7E082100AD5388 /* PBXContainerItemProxy */; + }; 7BEC18312AD5DA3300D30536 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4B2537592A11BE7300610219 /* NetworkProtectionSystemExtension */; @@ -12918,6 +13038,34 @@ }; name = Release; }; + 7BDA36F02B7E037200AD5388 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 7BDA36F12B7E037200AD5388 /* CI */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = CI; + }; + 7BDA36F22B7E037200AD5388 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 7BDA36F32B7E037200AD5388 /* Review */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Review; + }; 9D9AE8CD2AAA39A70026E7DC /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7B6EC5E52AE2D8AF004FE6DF /* DuckDuckGoDBPAgent.xcconfig */; @@ -13225,6 +13373,17 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7BDA36F42B7E037200AD5388 /* Build configuration list for PBXNativeTarget "VPNProxyExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7BDA36F02B7E037200AD5388 /* Debug */, + 7BDA36F12B7E037200AD5388 /* CI */, + 7BDA36F22B7E037200AD5388 /* Release */, + 7BDA36F32B7E037200AD5388 /* Review */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 9D9AE8CC2AAA39A70026E7DC /* Build configuration list for PBXNativeTarget "DuckDuckGoDBPBackgroundAgent" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -13379,7 +13538,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 109.0.0; + version = 109.0.1; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -13768,6 +13927,18 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = NetworkProtection; }; + 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7B31FD8B2AD125620086AA24 /* NetworkProtectionIPC */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionIPC; @@ -13784,10 +13955,36 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7B7DFB212B7E7473009EA1A3 /* Networking */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Networking; + }; 7B8C083B2AE1268E00F4C67F /* PixelKit */ = { isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B97CD5A2B7E0B85004FEF43 /* Common */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Common; + }; + 7B97CD612B7E0C4B004FEF43 /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + productName = PixelKit; + }; + 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */ = { isa = XCSwiftPackageProductDependency; productName = SystemExtensionManager; @@ -13806,6 +14003,10 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */ = { isa = XCSwiftPackageProductDependency; productName = SystemExtensionManager; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4796599b2d..e0a1c42442 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "5ecf4fe56f334be6eaecb65f6d55632a6d53921c", - "version" : "109.0.0" + "revision" : "da6a822844922401d80e26963b8b11dcd6ef221a", + "version" : "109.0.1" } }, { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index ebe3179a76..2f5d747ade 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -23,6 +23,7 @@ import Foundation import AppKit import Common import LoginItems +import NetworkProtectionProxy @MainActor final class DataBrokerProtectionDebugMenu: NSMenu { @@ -82,6 +83,11 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem(title: "Restart", action: #selector(DataBrokerProtectionDebugMenu.backgroundAgentRestart)) .targetting(self) + + NSMenuItem.separator() + + NSMenuItem(title: "Show agent IP address", action: #selector(DataBrokerProtectionDebugMenu.showAgentIPAddress)) + .targetting(self) } NSMenuItem(title: "Operations") { @@ -253,6 +259,10 @@ final class DataBrokerProtectionDebugMenu: NSMenu { window.delegate = self } + @objc private func showAgentIPAddress() { + DataBrokerProtectionManager.shared.showAgentIPAddress() + } + @objc private func showForceOptOutWindow() { let viewController = DataBrokerForceOptOutViewController() let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index f2c9bebd4d..75f8c3e855 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -41,8 +41,10 @@ public final class DataBrokerProtectionManager { return dataManager }() + private lazy var ipcClient = DataBrokerProtectionIPCClient(machServiceName: Bundle.main.dbpBackgroundAgentBundleId, pixelHandler: pixelHandler) + lazy var scheduler: DataBrokerProtectionLoginItemScheduler = { - let ipcClient = DataBrokerProtectionIPCClient(machServiceName: Bundle.main.dbpBackgroundAgentBundleId, pixelHandler: pixelHandler) + let ipcScheduler = DataBrokerProtectionIPCScheduler(ipcClient: ipcClient) return DataBrokerProtectionLoginItemScheduler(ipcScheduler: ipcScheduler, pixelHandler: pixelHandler) @@ -57,6 +59,12 @@ public final class DataBrokerProtectionManager { public func shouldAskForInviteCode() -> Bool { redeemUseCase.shouldAskForInviteCode() } + + // MARK: - Debugging Features + + public func showAgentIPAddress() { + ipcClient.openBrowser(domain: "https://www.whatismyip.com") + } } extension DataBrokerProtectionManager: DataBrokerProtectionDataManagerDelegate { diff --git a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift index e1e00e38c7..cdbfa623a9 100644 --- a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift +++ b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Foundation import LoginItems #if DBP diff --git a/DuckDuckGo/DuckDuckGo.entitlements b/DuckDuckGo/DuckDuckGo.entitlements index 7b79b8b2fe..757dc88e2c 100644 --- a/DuckDuckGo/DuckDuckGo.entitlements +++ b/DuckDuckGo/DuckDuckGo.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.developer.system-extension.install diff --git a/DuckDuckGo/DuckDuckGoAppStore.entitlements b/DuckDuckGo/DuckDuckGoAppStore.entitlements index e419bc0920..97443cb452 100644 --- a/DuckDuckGo/DuckDuckGoAppStore.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStore.entitlements @@ -19,6 +19,11 @@ com.apple.security.files.user-selected.read-write + com.apple.developer.networking.networkextension + + packet-tunnel-provider + app-proxy-provider + com.apple.security.network.client com.apple.security.personal-information.location diff --git a/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements b/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements index a2c7bd6bd5..13ea43d233 100644 --- a/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements @@ -2,10 +2,6 @@ - com.apple.developer.networking.networkextension - - packet-tunnel-provider - com.apple.security.app-sandbox com.apple.security.application-groups diff --git a/DuckDuckGo/DuckDuckGoDebug.entitlements b/DuckDuckGo/DuckDuckGoDebug.entitlements index dcffb16791..dad1686cba 100644 --- a/DuckDuckGo/DuckDuckGoDebug.entitlements +++ b/DuckDuckGo/DuckDuckGoDebug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.developer.system-extension.install diff --git a/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements b/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements deleted file mode 100644 index 069c866e05..0000000000 --- a/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements +++ /dev/null @@ -1,30 +0,0 @@ - - - - - com.apple.developer.networking.networkextension - - packet-tunnel-provider - - com.apple.developer.system-extension.install - - com.apple.developer.usernotifications.time-sensitive - - com.apple.security.application-groups - - HKE973VLUW.com.duckduckgo.network-protection - $(NETP_APP_GROUP) - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.personal-information.location - - keychain-access-groups - - $(AppIdentifierPrefix)com.duckduckgo.macos.browser - $(AppIdentifierPrefix)com.duckduckgo.network-protection - - - diff --git a/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements b/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements deleted file mode 100644 index a2226d1f8d..0000000000 --- a/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements +++ /dev/null @@ -1,38 +0,0 @@ - - - - - com.apple.developer.networking.networkextension - - packet-tunnel-provider-systemextension - - com.apple.developer.system-extension.install - - com.apple.developer.usernotifications.time-sensitive - - com.apple.security.application-groups - - $(TeamIdentifierPrefix)com.duckduckgo.macos.browser.network-protection - $(NETP_APP_GROUP) - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.personal-information.location - - keychain-access-groups - - $(AppIdentifierPrefix)com.duckduckgo.macos.browser - $(AppIdentifierPrefix)com.duckduckgo.network-protection - - com.apple.security.personal-information.location - - com.apple.developer.networking.networkextension - - packet-tunnel-provider-systemextension - - com.apple.developer.system-extension.install - - - diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index 4d7c2d94c0..70d7389fb7 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -8,7 +8,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "DuckDuckGo" + "value" : "DuckDuckGo Privacy Pro" } } } diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift new file mode 100644 index 0000000000..169f0ceb50 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift @@ -0,0 +1,65 @@ +// +// Bundle+VPN.swift +// +// Copyright © 2023 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 NetworkProtection + +extension Bundle { + + private enum VPNInfoKey: String { + case tunnelExtensionBundleID = "TUNNEL_EXTENSION_BUNDLE_ID" + case proxyExtensionBundleID = "PROXY_EXTENSION_BUNDLE_ID" + } + + static var tunnelExtensionBundleID: String { + string(for: .tunnelExtensionBundleID) + } + + static var proxyExtensionBundleID: String { + string(for: .proxyExtensionBundleID) + } + + private static func string(for key: VPNInfoKey) -> String { + guard let bundleID = Bundle.main.object(forInfoDictionaryKey: key.rawValue) as? String else { + fatalError("Info.plist is missing \(key)") + } + + return bundleID + } + +#if !NETWORK_EXTENSION + // for the Main or Launcher Agent app + static func mainAppBundle() -> Bundle { + return Bundle.main + } +#elseif NETP_SYSTEM_EXTENSION + // for the System Extension (Developer ID) + static func mainAppBundle() -> Bundle { + return Bundle(url: .mainAppBundleURL)! + } + // AppEx (App Store) can‘t access Main App Bundle +#endif + + static let keychainType: KeychainType = { +#if NETP_SYSTEM_EXTENSION + .system +#else + .dataProtection(.named(Bundle.main.appGroup(bundle: .netP))) +#endif + }() +} diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift deleted file mode 100644 index e14b7f1e84..0000000000 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// NetworkProtectionBundle.swift -// -// Copyright © 2023 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 NetworkProtection - -enum NetworkProtectionBundle { - -#if !NETWORK_EXTENSION - // for the Main or Launcher Agent app - static func mainAppBundle() -> Bundle { - return Bundle.main - } -#elseif NETP_SYSTEM_EXTENSION - // for the System Extension (Developer ID) - static func mainAppBundle() -> Bundle { - return Bundle(url: .mainAppBundleURL)! - } - // AppEx (App Store) can‘t access Main App Bundle -#endif - - static func extensionBundle() -> Bundle { -#if NETWORK_EXTENSION // When this code is compiled for any network-extension - return Bundle.main -#elseif NETP_SYSTEM_EXTENSION // When this code is compiled for the app when configured to use the sysex - let extensionsDirectoryURL = URL(fileURLWithPath: "Contents/Library/SystemExtensions", relativeTo: Bundle.main.bundleURL) - return extensionBundle(at: extensionsDirectoryURL) -#else // When this code is compiled for the app when configured to use the appex - let extensionsDirectoryURL = URL(fileURLWithPath: "Contents/Plugins", relativeTo: Bundle.main.bundleURL) - return extensionBundle(at: extensionsDirectoryURL) -#endif - } - - static func extensionBundle(at url: URL) -> Bundle { - let extensionURLs: [URL] - do { - extensionURLs = try FileManager.default.contentsOfDirectory(at: url, - includingPropertiesForKeys: nil, - options: .skipsHiddenFiles) - } catch let error { - fatalError("🔵 Failed to get the contents of \(url.absoluteString): \(error.localizedDescription)") - } - - // This should be updated to work well with other extensions - guard let extensionURL = extensionURLs.first else { - fatalError("🔵 Failed to find any system extensions") - } - - guard let extensionBundle = Bundle(url: extensionURL) else { - fatalError("🔵 Failed to create a bundle with URL \(extensionURL.absoluteString)") - } - - return extensionBundle - } - - static let keychainType: KeychainType = { -#if NETP_SYSTEM_EXTENSION - .system -#else - .dataProtection(.named(Bundle.main.appGroup(bundle: .netP))) -#endif - }() -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index e1696a7aba..f54236f387 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -22,6 +22,7 @@ import AppKit import Common import Foundation import NetworkProtection +import NetworkProtectionProxy import SwiftUI /// Controller for the Network Protection debug menu. @@ -29,6 +30,10 @@ import SwiftUI @MainActor final class NetworkProtectionDebugMenu: NSMenu { + private let transparentProxySettings = TransparentProxySettings(defaults: .netP) + + // MARK: - Menus + private let environmentMenu = NSMenu() private let preferredServerMenu: NSMenu @@ -39,7 +44,9 @@ final class NetworkProtectionDebugMenu: NSMenu { private let resetToDefaults = NSMenuItem(title: "Reset Settings to defaults", action: #selector(NetworkProtectionDebugMenu.resetSettings)) - private let exclusionsMenu = NSMenu() + private let excludedRoutesMenu = NSMenu() + private let excludeDDGBrowserTrafficFromVPN = NSMenuItem(title: "DDG Browser", action: #selector(toggleExcludeDDGBrowser)) + private let excludeDBPTrafficFromVPN = NSMenuItem(title: "DBP Background Agent", action: #selector(toggleExcludeDBPBackgroundAgent)) private let shouldEnforceRoutesMenuItem = NSMenuItem(title: "Kill Switch (enforceRoutes)", action: #selector(NetworkProtectionDebugMenu.toggleEnforceRoutesAction)) private let shouldIncludeAllNetworksMenuItem = NSMenuItem(title: "includeAllNetworks", action: #selector(NetworkProtectionDebugMenu.toggleIncludeAllNetworks)) @@ -89,7 +96,6 @@ final class NetworkProtectionDebugMenu: NSMenu { .targetting(self) shouldEnforceRoutesMenuItem .targetting(self) - NSMenuItem(title: "Excluded Routes").submenu(exclusionsMenu) NSMenuItem.separator() NSMenuItem(title: "Send Test Notification", action: #selector(NetworkProtectionDebugMenu.sendTestNotification)) @@ -104,6 +110,14 @@ final class NetworkProtectionDebugMenu: NSMenu { NSMenuItem(title: "Environment") .submenu(environmentMenu) + NSMenuItem(title: "Exclusions") { + NSMenuItem(title: "Excluded Apps") { + excludeDDGBrowserTrafficFromVPN.targetting(self) + excludeDBPTrafficFromVPN.targetting(self) + } + NSMenuItem(title: "Excluded Routes").submenu(excludedRoutesMenu) + } + NSMenuItem(title: "Preferred Server").submenu(preferredServerMenu) NSMenuItem(title: "Registration Key") { @@ -172,8 +186,8 @@ final class NetworkProtectionDebugMenu: NSMenu { populateNetworkProtectionServerListMenuItems() populateNetworkProtectionRegistrationKeyValidityMenuItems() - exclusionsMenu.delegate = self - exclusionsMenu.autoenablesItems = false + excludedRoutesMenu.delegate = self + excludedRoutesMenu.autoenablesItems = false populateExclusionsMenuItems() } @@ -391,7 +405,7 @@ final class NetworkProtectionDebugMenu: NSMenu { } private func populateExclusionsMenuItems() { - exclusionsMenu.removeAllItems() + excludedRoutesMenu.removeAllItems() for item in settings.excludedRoutes { let menuItem: NSMenuItem @@ -406,7 +420,7 @@ final class NetworkProtectionDebugMenu: NSMenu { target: self, representedObject: range.stringRepresentation) } - exclusionsMenu.addItem(menuItem) + excludedRoutesMenu.addItem(menuItem) } // Only allow testers to enter a custom code if they're on the waitlist, to simulate the correct path through the flow @@ -419,6 +433,7 @@ final class NetworkProtectionDebugMenu: NSMenu { override func update() { updateEnvironmentMenu() + updateExclusionsMenu() updatePreferredServerMenu() updateRekeyValidityMenu() updateNetworkProtectionMenuItemsState() @@ -588,6 +603,7 @@ final class NetworkProtectionDebugMenu: NSMenu { } // MARK: Environment + @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { let title = menuItem.title let selectedEnvironment: VPNSettings.SelectedEnvironment @@ -608,6 +624,24 @@ final class NetworkProtectionDebugMenu: NSMenu { settings.selectedServer = .automatic } } + + // MARK: - Exclusions + + private let dbpBackgroundAppIdentifier = Bundle.main.dbpBackgroundAgentBundleId + private let ddgBrowserAppIdentifier = Bundle.main.bundleIdentifier! + + private func updateExclusionsMenu() { + excludeDBPTrafficFromVPN.state = transparentProxySettings.isExcluding(dbpBackgroundAppIdentifier) ? .on : .off + excludeDDGBrowserTrafficFromVPN.state = transparentProxySettings.isExcluding(ddgBrowserAppIdentifier) ? .on : .off + } + + @objc private func toggleExcludeDBPBackgroundAgent() { + transparentProxySettings.toggleExclusion(for: dbpBackgroundAppIdentifier) + } + + @objc private func toggleExcludeDDGBrowser() { + transparentProxySettings.toggleExclusion(for: ddgBrowserAppIdentifier) + } } extension NetworkProtectionDebugMenu: NSMenuDelegate { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index f67223a545..0bd4975196 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -24,6 +24,7 @@ import SwiftUI import Common import NetworkExtension import NetworkProtection +import NetworkProtectionProxy import NetworkProtectionUI import Networking import PixelKit @@ -38,6 +39,8 @@ typealias NetworkProtectionConfigChangeHandler = () -> Void final class NetworkProtectionTunnelController: TunnelController, TunnelSessionProvider { + // MARK: - Settings + let settings: VPNSettings // MARK: - Combine Cancellables @@ -60,6 +63,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// private let controllerErrorStore = NetworkProtectionControllerErrorStore() + private let notificationCenter: NotificationCenter + // MARK: - VPN Tunnel & Configuration /// Auth token store @@ -95,6 +100,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Loads the configuration matching our ``extensionID``. /// + @MainActor public var manager: NETunnelProviderManager? { get async { if let internalManager { @@ -139,13 +145,14 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr init(networkExtensionBundleID: String, networkExtensionController: NetworkExtensionController, settings: VPNSettings, - notificationCenter: NotificationCenter = .default, tokenStore: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), + notificationCenter: NotificationCenter = .default, logger: NetworkProtectionLogger = DefaultNetworkProtectionLogger()) { self.logger = logger self.networkExtensionBundleID = networkExtensionBundleID self.networkExtensionController = networkExtensionController + self.notificationCenter = notificationCenter self.settings = settings self.tokenStore = tokenStore @@ -254,7 +261,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr tunnelManager.protocolConfiguration = { let protocolConfiguration = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() protocolConfiguration.serverAddress = "127.0.0.1" // Dummy address... the NetP service will take care of grabbing a real server - protocolConfiguration.providerBundleIdentifier = NetworkProtectionBundle.extensionBundle().bundleIdentifier + protocolConfiguration.providerBundleIdentifier = Bundle.tunnelExtensionBundleID protocolConfiguration.providerConfiguration = [ NetworkProtectionOptionKey.defaultPixelHeaders: APIRequest.Headers().httpHeaders, NetworkProtectionOptionKey.includedRoutes: includedRoutes().map(\.stringRepresentation) as NSArray @@ -304,6 +311,14 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } } + // MARK: - Connection + + public var status: NEVPNStatus { + get async { + await connection?.status ?? .disconnected + } + } + // MARK: - Connection Status Querying /// Queries Network Protection to know if its VPN is connected. diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 5770b78a2f..3a3a392736 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -224,7 +224,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) - let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: NetworkProtectionBundle.keychainType, + let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, serviceName: Self.tokenServiceName, errorEvents: debugEvents) let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings) @@ -232,7 +232,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { super.init(notificationsPresenter: notificationsPresenter, tunnelHealthStore: tunnelHealthStore, controllerErrorStore: controllerErrorStore, - keychainType: NetworkProtectionBundle.keychainType, + keychainType: Bundle.keychainType, tokenStore: tokenStore, debugEvents: debugEvents, providerEvents: Self.packetTunnelProviderEvents, @@ -323,13 +323,6 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { case missingPixelHeaders } - override func prepareToConnect(using provider: NETunnelProviderProtocol?) { - super.prepareToConnect(using: provider) - - guard PixelKit.shared == nil, let options = provider?.providerConfiguration else { return } - try? loadDefaultPixelHeaders(from: options) - } - public override func loadVendorOptions(from provider: NETunnelProviderProtocol?) throws { try super.loadVendorOptions(from: provider) @@ -350,6 +343,15 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { setupPixels(defaultHeaders: defaultPixelHeaders) } + // MARK: - Overrideable Connection Events + + override func prepareToConnect(using provider: NETunnelProviderProtocol?) { + super.prepareToConnect(using: provider) + + guard PixelKit.shared == nil, let options = provider?.providerConfiguration else { return } + try? loadDefaultPixelHeaders(from: options) + } + // MARK: - Start/Stop Tunnel override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift new file mode 100644 index 0000000000..d300309ec6 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift @@ -0,0 +1,94 @@ +// +// MacTransparentProxyProvider.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 Common +import Foundation +import Networking +import NetworkExtension +import NetworkProtectionProxy +import os.log // swiftlint:disable:this enforce_os_log_wrapper +import PixelKit + +final class MacTransparentProxyProvider: TransparentProxyProvider { + + static var vpnProxyLogger = Logger(subsystem: OSLog.subsystem, category: "VPN Proxy") + + private var cancellables = Set() + + @objc init() { + let loadSettingsFromStartupOptions: Bool = { +#if NETP_SYSTEM_EXTENSION + true +#else + false +#endif + }() + + let settings: TransparentProxySettings = { +#if NETP_SYSTEM_EXTENSION + /// Because our System Extension is running in the system context and doesn't have access + /// to shared user defaults, we just make it use the `.standard` defaults. + TransparentProxySettings(defaults: .standard) +#else + /// Because our App Extension is running in the user context and has access + /// to shared user defaults, we take advantage of this and use the `.netP` defaults. + TransparentProxySettings(defaults: .netP) +#endif + }() + + let configuration = TransparentProxyProvider.Configuration( + loadSettingsFromProviderConfiguration: loadSettingsFromStartupOptions) + + super.init(settings: settings, + configuration: configuration, + logger: Self.vpnProxyLogger) + + eventHandler = eventHandler(_:) + +#if !NETP_SYSTEM_EXTENSION + let dryRun: Bool +#if DEBUG + dryRun = true +#else + dryRun = false +#endif + + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: "vpnProxyExtension", + defaultHeaders: [:], + log: .networkProtectionPixel, + defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequest.Headers(additionalHeaders: headers) + let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) + let request = APIRequest(configuration: configuration) + + request.fetch { _, error in + onComplete(error == nil, error) + } + } +#endif + } + + private func eventHandler(_ event: TransparentProxyProvider.Event) { + PixelKit.fire(event) + } +} diff --git a/DuckDuckGo/NetworkProtectionAppExtension.entitlements b/DuckDuckGo/NetworkProtectionAppExtension.entitlements index 13dd983ca1..d37610bb07 100644 --- a/DuckDuckGo/NetworkProtectionAppExtension.entitlements +++ b/DuckDuckGo/NetworkProtectionAppExtension.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.security.app-sandbox diff --git a/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift b/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift new file mode 100644 index 0000000000..85891c6604 --- /dev/null +++ b/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift @@ -0,0 +1,64 @@ +// +// BrowserWindowManager.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 AppKit +import Foundation +import WebKit + +/// A class that offers functionality to quickly show an interactive browser window. +/// +/// This class is meant to aid with debugging and should not be included in release builds. +/// . +final class BrowserWindowManager: NSObject { + private var interactiveBrowserWindow: NSWindow? + + @MainActor + func show(domain: String) { + if let interactiveBrowserWindow, interactiveBrowserWindow.isVisible { + return + } + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, defer: false) + window.center() + window.title = "Web Browser" + window.delegate = self + interactiveBrowserWindow = window + + // Create the WKWebView. + let webView = WKWebView(frame: window.contentView!.bounds) + webView.autoresizingMask = [.width, .height] + window.contentView!.addSubview(webView) + + // Load a URL. + let url = URL(string: domain)! + let request = URLRequest(url: url) + webView.load(request) + + // Show the window. + window.makeKeyAndOrderFront(nil) + } +} + +extension BrowserWindowManager: NSWindowDelegate { + func windowWillClose(_ notification: Notification) { + interactiveBrowserWindow = nil + } +} diff --git a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift index 1d7c0403fb..c452200f7b 100644 --- a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift @@ -17,10 +17,10 @@ // import Combine -import Foundation +import Common import DataBrokerProtection +import Foundation import PixelKit -import Common /// Manages the IPC service for the Agent app /// @@ -28,6 +28,7 @@ import Common /// demand interaction with. /// final class IPCServiceManager { + private var browserWindowManager: BrowserWindowManager private let ipcServer: DataBrokerProtectionIPCServer private let scheduler: DataBrokerProtectionScheduler private let pixelHandler: EventMapping @@ -41,6 +42,8 @@ final class IPCServiceManager { self.scheduler = scheduler self.pixelHandler = pixelHandler + browserWindowManager = BrowserWindowManager() + ipcServer.serverDelegate = self ipcServer.activate() } @@ -102,4 +105,10 @@ extension IPCServiceManager: IPCServerInterface { pixelHandler.fire(.ipcServerRunAllOperations) scheduler.runAllOperations(showWebView: showWebView) } + + func openBrowser(domain: String) { + Task { @MainActor in + browserWindowManager.show(domain: domain) + } + } } diff --git a/DuckDuckGoVPN/DuckDuckGoVPN.entitlements b/DuckDuckGoVPN/DuckDuckGoVPN.entitlements index 653311b9ec..2797c3f947 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPN.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPN.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.developer.system-extension.install diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index add4af8a6d..f14bbac165 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -23,7 +23,7 @@ import LoginItems import Networking import NetworkExtension import NetworkProtection -import NetworkProtectionIPC +import NetworkProtectionProxy import NetworkProtectionUI import ServiceManagement import PixelKit @@ -60,18 +60,82 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private var cancellables = Set() - var networkExtensionBundleID: String { - Bundle.main.networkExtensionBundleID + var proxyExtensionBundleID: String { + Bundle.proxyExtensionBundleID } -#if NETWORK_PROTECTION - private lazy var networkExtensionController = NetworkExtensionController(extensionBundleID: networkExtensionBundleID) + var tunnelExtensionBundleID: String { + Bundle.tunnelExtensionBundleID + } + + private lazy var networkExtensionController = NetworkExtensionController(extensionBundleID: tunnelExtensionBundleID) + + private var storeProxySettingsInProviderConfiguration: Bool { +#if NETP_SYSTEM_EXTENSION + true +#else + false #endif + } private lazy var tunnelSettings = VPNSettings(defaults: .netP) + private lazy var proxySettings = TransparentProxySettings(defaults: .netP) + + @MainActor + private lazy var vpnProxyLauncher = VPNProxyLauncher( + tunnelController: tunnelController, + proxyController: proxyController) + + @MainActor + private lazy var proxyController: TransparentProxyController = { + let controller = TransparentProxyController( + extensionID: proxyExtensionBundleID, + storeSettingsInProviderConfiguration: storeProxySettingsInProviderConfiguration, + settings: proxySettings) { [weak self] manager in + guard let self else { return } + + manager.localizedDescription = "DuckDuckGo VPN Proxy" + + if !manager.isEnabled { + manager.isEnabled = true + } + + manager.protocolConfiguration = { + let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() + protocolConfiguration.serverAddress = "127.0.0.1" // Dummy address... the NetP service will take care of grabbing a real server + protocolConfiguration.providerBundleIdentifier = self.proxyExtensionBundleID + + // always-on + protocolConfiguration.disconnectOnSleep = false + + // kill switch + // protocolConfiguration.enforceRoutes = false + + // this setting breaks Connection Tester + // protocolConfiguration.includeAllNetworks = settings.includeAllNetworks + + // This is intentionally not used but left here for documentation purposes. + // The reason for this is that we want to have full control of the routes that + // are excluded, so instead of using this setting we're just configuring the + // excluded routes through our VPNSettings class, which our extension reads directly. + // protocolConfiguration.excludeLocalNetworks = settings.excludeLocalNetworks + return protocolConfiguration + }() + } + + controller.eventHandler = handleControllerEvent(_:) + + return controller + }() + + private func handleControllerEvent(_ event: TransparentProxyController.Event) { + PixelKit.fire(event) + } + + @MainActor private lazy var tunnelController = NetworkProtectionTunnelController( - networkExtensionBundleID: networkExtensionBundleID, + networkExtensionBundleID: tunnelExtensionBundleID, networkExtensionController: networkExtensionController, settings: tunnelSettings) @@ -79,6 +143,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { /// /// This is used by our main app to control the tunnel through the VPN login item. /// + @MainActor private lazy var tunnelControllerIPCService: TunnelControllerIPCService = { let ipcServer = TunnelControllerIPCService( tunnelController: tunnelController, @@ -88,17 +153,19 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { return ipcServer }() + @MainActor + private lazy var statusObserver = ConnectionStatusObserverThroughSession( + tunnelSessionProvider: tunnelController, + platformNotificationCenter: NSWorkspace.shared.notificationCenter, + platformDidWakeNotification: NSWorkspace.didWakeNotification) + + @MainActor private lazy var statusReporter: NetworkProtectionStatusReporter = { let errorObserver = ConnectionErrorObserverThroughSession( tunnelSessionProvider: tunnelController, platformNotificationCenter: NSWorkspace.shared.notificationCenter, platformDidWakeNotification: NSWorkspace.didWakeNotification) - let statusObserver = ConnectionStatusObserverThroughSession( - tunnelSessionProvider: tunnelController, - platformNotificationCenter: NSWorkspace.shared.notificationCenter, - platformDidWakeNotification: NSWorkspace.didWakeNotification) - let serverInfoObserver = ConnectionServerInfoObserverThroughSession( tunnelSessionProvider: tunnelController, platformNotificationCenter: NSWorkspace.shared.notificationCenter, @@ -113,6 +180,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { ) }() + @MainActor private lazy var vpnAppEventsHandler = { VPNAppEventsHandler(tunnelController: tunnelController) }() @@ -175,8 +243,9 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { bouncer.requireAuthTokenOrKillApp() - // Initialize the IPC server + // Initialize lazy properties _ = tunnelControllerIPCService + _ = vpnProxyLauncher let dryRun: Bool diff --git a/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements b/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements index a6ed34f64f..f531d0bc0c 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.developer.system-extension.install diff --git a/DuckDuckGoVPN/Info-AppStore.plist b/DuckDuckGoVPN/Info-AppStore.plist index 7627fdd9c9..a1b2b02a02 100644 --- a/DuckDuckGoVPN/Info-AppStore.plist +++ b/DuckDuckGoVPN/Info-AppStore.plist @@ -6,8 +6,10 @@ $(DISTRIBUTED_NOTIFICATIONS_PREFIX) NETP_APP_GROUP $(NETP_APP_GROUP) - SYSEX_BUNDLE_ID - $(SYSEX_BUNDLE_ID) + PROXY_EXTENSION_BUNDLE_ID + $(PROXY_EXTENSION_BUNDLE_ID) + TUNNEL_EXTENSION_BUNDLE_ID + $(TUNNEL_EXTENSION_BUNDLE_ID) LSApplicationCategoryType public.app-category.productivity CFBundleShortVersionString diff --git a/DuckDuckGoVPN/Info.plist b/DuckDuckGoVPN/Info.plist index 7627fdd9c9..a1b2b02a02 100644 --- a/DuckDuckGoVPN/Info.plist +++ b/DuckDuckGoVPN/Info.plist @@ -6,8 +6,10 @@ $(DISTRIBUTED_NOTIFICATIONS_PREFIX) NETP_APP_GROUP $(NETP_APP_GROUP) - SYSEX_BUNDLE_ID - $(SYSEX_BUNDLE_ID) + PROXY_EXTENSION_BUNDLE_ID + $(PROXY_EXTENSION_BUNDLE_ID) + TUNNEL_EXTENSION_BUNDLE_ID + $(TUNNEL_EXTENSION_BUNDLE_ID) LSApplicationCategoryType public.app-category.productivity CFBundleShortVersionString diff --git a/DuckDuckGoVPN/VPNProxyLauncher.swift b/DuckDuckGoVPN/VPNProxyLauncher.swift new file mode 100644 index 0000000000..c99d187cf2 --- /dev/null +++ b/DuckDuckGoVPN/VPNProxyLauncher.swift @@ -0,0 +1,149 @@ +// +// VPNProxyLauncher.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 NetworkProtectionProxy +import NetworkExtension + +/// Starts and stops the VPN proxy component. +/// +/// This class looks at the tunnel and the proxy components and their status and settings, and decides based on +/// a number of conditions whether to start the proxy, stop it, or just leave it be. +/// +@MainActor +final class VPNProxyLauncher { + private let tunnelController: NetworkProtectionTunnelController + private let proxyController: TransparentProxyController + private let notificationCenter: NotificationCenter + private var cancellables = Set() + + init(tunnelController: NetworkProtectionTunnelController, + proxyController: TransparentProxyController, + notificationCenter: NotificationCenter = .default) { + + self.notificationCenter = notificationCenter + self.proxyController = proxyController + self.tunnelController = tunnelController + + subscribeToStatusChanges() + subscribeToProxySettingChanges() + } + + // MARK: - Status Changes + + private func subscribeToStatusChanges() { + notificationCenter.publisher(for: .NEVPNStatusDidChange) + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink(receiveValue: statusChanged(notification:)) + .store(in: &cancellables) + } + + private func statusChanged(notification: Notification) { + Task { @MainActor in + let isProxyConnectionStatusChange = await proxyController.connection == notification.object as? NEVPNConnection + + try await startOrStopProxyIfNeeded(isProxyConnectionStatusChange: isProxyConnectionStatusChange) + } + } + + // MARK: - Proxy Settings Changes + + private func subscribeToProxySettingChanges() { + proxyController.settings.changePublisher + .sink(receiveValue: proxySettingChanged(_:)) + .store(in: &cancellables) + } + + private func proxySettingChanged(_ change: TransparentProxySettings.Change) { + Task { @MainActor in + try await startOrStopProxyIfNeeded() + } + } + + // MARK: - Auto starting & stopping the proxy component + + private var isControllingProxy = false + + private func startOrStopProxyIfNeeded(isProxyConnectionStatusChange: Bool = false) async throws { + if await shouldStartProxy { + guard !isControllingProxy else { + return + } + + isControllingProxy = true + + // When we're auto-starting the proxy because its own status changed to + // disconnected, we want to give it a pause because if it fails to connect again + // we risk the proxy entering a frenetic connect / disconnect loop + if isProxyConnectionStatusChange { + // If the proxy connection was stopped, let's wait a bit before trying to enable it again + try await Task.sleep(interval: .seconds(10)) + + // And we want to check again if the proxy still needs to start after waiting + guard await shouldStartProxy else { + return + } + } + + do { + try await proxyController.start() + isControllingProxy = false + } catch { + isControllingProxy = false + throw error + } + } else if await shouldStopProxy { + guard !isControllingProxy else { + return + } + + isControllingProxy = true + await proxyController.stop() + isControllingProxy = false + } + } + + private var shouldStartProxy: Bool { + get async { + let proxyIsDisconnected = await proxyController.status == .disconnected + let tunnelIsConnected = await tunnelController.status == .connected + + // Starting the proxy only when it's required for active features + // is a product decision. It may change once we decide the proxy + // is stable enough to be running at all times. + return proxyIsDisconnected + && tunnelIsConnected + && proxyController.isRequiredForActiveFeatures + } + } + + private var shouldStopProxy: Bool { + get async { + let proxyIsConnected = await proxyController.status == .connected + let tunnelIsDisconnected = await tunnelController.status == .disconnected + + // Stopping the proxy when it's not required for active features + // is a product decision. It may change once we decide the proxy + // is stable enough to be running at all times. + return proxyIsConnected + && (tunnelIsDisconnected || !proxyController.isRequiredForActiveFeatures) + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index fb62529ee7..64fa721012 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 4a6f58ac28..2aa3953437 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -17,9 +17,9 @@ // import Combine +import Common import Foundation import XPCHelper -import Common /// This protocol describes the server-side IPC interface for controlling the tunnel /// @@ -150,6 +150,17 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { // If you add a completion block, please remember to call it here too! }) } + + public func openBrowser(domain: String) { + self.pixelHandler.fire(.ipcServerRunAllOperations) + xpc.execute(call: { server in + server.openBrowser(domain: domain) + }, xpcReplyErrorHandler: { error in + os_log("Error \(error.localizedDescription)") + // Intentional no-op as there's no completion block + // If you add a completion block, please remember to call it here too! + }) + } } // MARK: - Incoming communication from the server diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index fde0274a5f..a2bc3d0e56 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -42,6 +42,12 @@ public protocol IPCServerInterface: AnyObject { func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runAllOperations(showWebView: Bool) + + // MARK: - Debugging Features + + /// Opens a browser window with the specified domain + /// + func openBrowser(domain: String) } /// This protocol describes the server-side XPC interface. @@ -71,6 +77,12 @@ protocol XPCServerInterface { func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runAllOperations(showWebView: Bool) + + // MARK: - Debugging Features + + /// Opens a browser window with the specified domain + /// + func openBrowser(domain: String) } public final class DataBrokerProtectionIPCServer { @@ -146,4 +158,8 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { func runAllOperations(showWebView: Bool) { serverDelegate?.runAllOperations(showWebView: showWebView) } + + func openBrowser(domain: String) { + serverDelegate?.openBrowser(domain: domain) + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index ad3dfda484..9f12bbab0c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -279,7 +279,6 @@ public enum DataBrokerProtectionPixels { } extension DataBrokerProtectionPixels: PixelKitEvent { - public var name: String { switch self { case .parentChildMatches: return "m_mac_dbp_macos_parent-child-broker-matches" diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index bbe7e894b8..a631a199d9 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -13,7 +13,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Package.resolved b/LocalPackages/NetworkProtectionMac/Package.resolved new file mode 100644 index 0000000000..08c5add0f4 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Package.resolved @@ -0,0 +1,104 @@ +{ + "pins" : [ + { + "identity" : "bloom_cpp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/bloom_cpp.git", + "state" : { + "revision" : "8076199456290b61b4544bf2f4caf296759906a0", + "version" : "3.0.0" + } + }, + { + "identity" : "browserserviceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/BrowserServicesKit", + "state" : { + "revision" : "1f7932fe67a0d8b1ae97e62cb333639353d4772f", + "version" : "101.2.2" + } + }, + { + "identity" : "content-scope-scripts", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/content-scope-scripts", + "state" : { + "revision" : "0b68b0d404d8d4f32296cd84fa160b18b0aeaf44", + "version" : "4.59.1" + } + }, + { + "identity" : "duckduckgo-autofill", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", + "state" : { + "revision" : "b972bc0ab6ee1d57a0a18a197dcc31e40ae6ac57", + "version" : "10.0.3" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/GRDB.swift.git", + "state" : { + "revision" : "9f049d7b97b1e68ffd86744b500660d34a9e79b8", + "version" : "2.3.0" + } + }, + { + "identity" : "privacy-dashboard", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/privacy-dashboard", + "state" : { + "revision" : "38336a574e13090764ba09a6b877d15ee514e371", + "version" : "3.1.1" + } + }, + { + "identity" : "punycodeswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gumob/PunycodeSwift.git", + "state" : { + "revision" : "4356ec54e073741449640d3d50a1fd24fd1e1b8b", + "version" : "2.1.0" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version" : "1.3.0" + } + }, + { + "identity" : "sync_crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/sync_crypto", + "state" : { + "revision" : "2ab6ab6f0f96b259c14c2de3fc948935fc16ac78", + "version" : "0.2.0" + } + }, + { + "identity" : "trackerradarkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "state" : { + "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", + "version" : "1.2.2" + } + }, + { + "identity" : "wireguard-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/wireguard-apple", + "state" : { + "revision" : "2d8172c11478ab11b0f5ad49bdb4f93f4b3d5e0d", + "version" : "1.1.1" + } + } + ], + "version" : 2 +} diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index a1edbe7388..e2dc908671 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -27,10 +27,11 @@ let package = Package( ], products: [ .library(name: "NetworkProtectionIPC", targets: ["NetworkProtectionIPC"]), + .library(name: "NetworkProtectionProxy", targets: ["NetworkProtectionProxy"]), .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems") @@ -50,6 +51,19 @@ let package = Package( plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] ), + // MARK: - NetworkProtectionProxy + + .target( + name: "NetworkProtectionProxy", + dependencies: [ + .product(name: "NetworkProtection", package: "BrowserServicesKit") + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] + ), + // MARK: - NetworkProtectionUI .target( diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift new file mode 100644 index 0000000000..882eb19734 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift @@ -0,0 +1,242 @@ +// +// TCPFlowManager.swift +// +// Copyright © 2023 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 NetworkExtension +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// A private global actor to handle UDP flows management +/// +@globalActor +struct TCPFlowActor { + actor ActorType { } + + static let shared: ActorType = ActorType() +} + +@TCPFlowActor +enum RemoteConnectionError: Error { + case complete + case cancelled + case couldNotEstablishConnection(_ error: Error) + case unhandledError(_ error: Error) +} + +final class TCPFlowManager { + private let flow: NEAppProxyTCPFlow + private var connectionTask: Task? + private var connection: NWConnection? + + init(flow: NEAppProxyTCPFlow) { + self.flow = flow + } + + deinit { + // Just making extra sure we don't have any unexpected retain cycle + connection?.stateUpdateHandler = nil + connection?.cancel() + } + + func start(interface: NWInterface) async throws { + guard let remoteEndpoint = flow.remoteEndpoint as? NWHostEndpoint else { + return + } + + try await connectAndStartRunLoop(remoteEndpoint: remoteEndpoint, interface: interface) + } + + private func connectAndStartRunLoop(remoteEndpoint: NWHostEndpoint, interface: NWInterface) async throws { + let remoteConnection = try await connect(to: remoteEndpoint, interface: interface) + try await flow.open(withLocalEndpoint: nil) + + do { + try await startDataCopyLoop(for: remoteConnection) + + remoteConnection.cancel() + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + } catch { + remoteConnection.cancel() + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + } + + func connect(to remoteEndpoint: NWHostEndpoint, interface: NWInterface) async throws -> NWConnection { + try await withCheckedThrowingContinuation { continuation in + connect(to: remoteEndpoint, interface: interface) { result in + switch result { + case .success(let connection): + continuation.resume(returning: connection) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + func connect(to remoteEndpoint: NWHostEndpoint, interface: NWInterface, completion: @escaping @TCPFlowActor (Result) -> Void) { + let host = Network.NWEndpoint.Host(remoteEndpoint.hostname) + let port = Network.NWEndpoint.Port(remoteEndpoint.port)! + + let parameters = NWParameters.tcp + parameters.preferNoProxies = true + parameters.requiredInterface = interface + parameters.prohibitedInterfaceTypes = [.other] + + let connection = NWConnection(host: host, port: port, using: parameters) + self.connection = connection + + connection.stateUpdateHandler = { (state: NWConnection.State) in + Task { @TCPFlowActor in + switch state { + case .ready: + connection.stateUpdateHandler = nil + completion(.success(connection)) + case .cancelled: + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.cancelled)) + case .failed(let error), .waiting(let error): + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.couldNotEstablishConnection(error))) + default: + break + } + } + } + + connection.start(queue: .global()) + } + + private func startDataCopyLoop(for remoteConnection: NWConnection) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { [weak self] in + while true { + guard let self else { + throw RemoteConnectionError.cancelled + } + + try Task.checkCancellation() + try await self.copyOutboundTraffic(to: remoteConnection) + } + } + + group.addTask { [weak self] in + while true { + guard let self else { + throw RemoteConnectionError.cancelled + } + + try Task.checkCancellation() + try await self.copyInboundTraffic(from: remoteConnection) + } + } + + while !group.isEmpty { + do { + try await group.next() + + } catch { + group.cancelAll() + throw error + } + } + } + } + + @MainActor + func closeFlow(remoteConnection: NWConnection, error: Error?) { + remoteConnection.forceCancel() + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + + static let maxReceiveSize: Int = Int(Measurement(value: 2, unit: UnitInformationStorage.megabytes).converted(to: .bytes).value) + + func copyInboundTraffic(from remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @TCPFlowActor in + remoteConnection.receive(minimumIncompleteLength: 1, + maximumLength: Self.maxReceiveSize) { [weak flow] (data, _, isComplete, error) in + guard let flow else { + continuation.resume(throwing: RemoteConnectionError.cancelled) + return + } + + switch (data, isComplete, error) { + case (.some(let data), _, _) where !data.isEmpty: + flow.write(data) { writeError in + if let writeError { + continuation.resume(throwing: writeError) + remoteConnection.cancel() + } else { + continuation.resume() + } + } + case (_, isComplete, _) where isComplete == true: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + case (_, _, .some(let error)): + continuation.resume(throwing: RemoteConnectionError.unhandledError(error)) + remoteConnection.cancel() + default: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + } + } + } + } + } + + func copyOutboundTraffic(to remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @TCPFlowActor in + flow.readData { data, error in + switch (data, error) { + case (.some(let data), _) where !data.isEmpty: + remoteConnection.send(content: data, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + remoteConnection.cancel() + return + } + + continuation.resume() + })) + case (_, .some(let error)): + continuation.resume(throwing: error) + remoteConnection.cancel() + default: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + } + } + } + } + } +} + +extension TCPFlowManager: Hashable { + static func == (lhs: TCPFlowManager, rhs: TCPFlowManager) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(flow) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift new file mode 100644 index 0000000000..000f37d20e --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift @@ -0,0 +1,329 @@ +// +// UDPFlowManager.swift +// +// Copyright © 2023 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 NetworkExtension +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// A private global actor to handle UDP flows management +/// +@globalActor +struct UDPFlowActor { + actor ActorType { } + + static let shared: ActorType = ActorType() +} + +/// Class to handle UDP connections +/// +/// This is necessary because as described in the reference comment for this implementation (see ``UDPFlowManager``'s documentation) +/// it's noted that a single UDP flow can have to manage multiple connections. +/// +@UDPFlowActor +final class UDPConnectionManager { + let endpoint: NWEndpoint + private let connection: NWConnection + private let onReceive: (_ endpoint: NWEndpoint, _ result: Result) async -> Void + + init(endpoint: NWHostEndpoint, interface: NWInterface?, onReceive: @UDPFlowActor @escaping (_ endpoint: NWEndpoint, _ result: Result) async -> Void) { + let host = Network.NWEndpoint.Host(endpoint.hostname) + let port = Network.NWEndpoint.Port(endpoint.port)! + + let parameters = NWParameters.udp + parameters.preferNoProxies = true + parameters.requiredInterface = interface + parameters.prohibitedInterfaceTypes = [.other] + + let connection = NWConnection(host: host, port: port, using: parameters) + + self.connection = connection + self.endpoint = endpoint + self.onReceive = onReceive + } + + deinit { + // Just making extra sure we don't retain anything we don't need to + connection.stateUpdateHandler = nil + connection.cancel() + } + + // MARK: - General Operation + + /// Starts the operation of this connection manager + /// + /// Can be called multiple times safely. + /// + private func start() async throws { + guard connection.state == .setup else { + return + } + + try await connect() + + Task { + while true { + do { + let datagram = try await receive() + await onReceive(endpoint, .success(datagram)) + } catch { + connection.cancel() + await onReceive(endpoint, .failure(error)) + break + } + } + } + } + + // MARK: - Connection Management + + private func connect() async throws { + try await withCheckedThrowingContinuation { continuation in + connect { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + private func connect(completion: @escaping (Result) -> Void) { + connection.stateUpdateHandler = { [connection] (state: NWConnection.State) in + switch state { + case .ready: + connection.stateUpdateHandler = nil + completion(.success(())) + case .cancelled: + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.cancelled)) + case .failed(let error), .waiting(let error): + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.couldNotEstablishConnection(error))) + default: + break + } + } + + connection.start(queue: .global()) + } + + // MARK: - Receiving from remote + + private func receive() async throws -> Data { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.receiveMessage { [weak self] data, _, isComplete, error in + + guard self != nil else { + continuation.resume(throwing: RemoteConnectionError.cancelled) + return + } + + switch (data, isComplete, error) { + case (let data?, _, _): + continuation.resume(returning: data) + case (_, true, _): + continuation.resume(throwing: RemoteConnectionError.cancelled) + case (_, _, let error?): + continuation.resume(throwing: RemoteConnectionError.unhandledError(error)) + default: + continuation.resume(throwing: RemoteConnectionError.cancelled) + } + } + } + } + + // MARK: - Writing datagrams + + func write(datagram: Data) async throws { + try await start() + + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.send(content: datagram, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + })) + } + } +} + +extension UDPConnectionManager: Hashable, Equatable { + // MARK: - Equatable + + static func == (lhs: UDPConnectionManager, rhs: UDPConnectionManager) -> Bool { + lhs.endpoint == rhs.endpoint + } + + // MARK: - Hashable + + func hash(into hasher: inout Hasher) { + hasher.combine(endpoint) + } +} + +/// UDP flow manager class +/// +/// There is documentation explaining how to handle TCP flows here: +/// https://developer.apple.com/documentation/networkextension/app_proxy_provider/handling_flow_copying?changes=_8 +/// +/// Unfortunately there isn't good official documentation showcasing how to implement UDP flow management. +/// The best we could fine are two comments by an Apple engineer that shine some light on how that implementation should be like: +/// https://developer.apple.com/forums/thread/678464?answerId=671531022#671531022 +/// https://developer.apple.com/forums/thread/678464?answerId=671892022#671892022 +/// +/// This class is the result of implementing the description found in that comment. +/// +@UDPFlowActor +final class UDPFlowManager { + private let flow: NEAppProxyUDPFlow + private var interface: NWInterface? + + private var connectionManagers = [NWEndpoint: UDPConnectionManager]() + + init(flow: NEAppProxyUDPFlow) { + self.flow = flow + } + + func start(interface: NWInterface) async throws { + self.interface = interface + try await connectAndStartRunLoop() + } + + private func connectAndStartRunLoop() async throws { + do { + try await flow.open(withLocalEndpoint: nil) + try await startDataCopyLoop() + + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + } catch { + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + } + + private func startDataCopyLoop() async throws { + while true { + try await copyOutoundTraffic() + } + } + + func copyInboundTraffic(endpoint: NWEndpoint, result: Result) async { + switch result { + case .success(let data): + do { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + flow.writeDatagrams([data], sentBy: [endpoint]) { error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + } + } + } catch { + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + case .failure: + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + } + + func copyOutoundTraffic() async throws { + let (datagrams, endpoints) = try await read() + + // Ref: https://developer.apple.com/documentation/networkextension/neappproxyudpflow/1406576-readdatagrams + if datagrams.isEmpty || endpoints.isEmpty { + throw NEAppProxyFlowError(.aborted) + } + + for (datagram, endpoint) in zip(datagrams, endpoints) { + guard let endpoint = endpoint as? NWHostEndpoint else { + // Not sure what to do about this... + continue + } + + let manager = connectionManagers[endpoint] ?? { + let manager = UDPConnectionManager(endpoint: endpoint, interface: interface, onReceive: copyInboundTraffic) + connectionManagers[endpoint] = manager + return manager + }() + + do { + try await manager.write(datagram: datagram) + } catch { + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + } + } + + /// Reads datagrams from the flow. + /// + /// Apple's documentation is very bad here, but it seems each datagram is corresponded with an endpoint at the same position in the array + /// as mentioned here: https://developer.apple.com/forums/thread/75893 + /// + private func read() async throws -> (datagrams: [Data], endpoints: [NWEndpoint]) { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<([Data], [NWEndpoint]), Error>) in + flow.readDatagrams { datagrams, endpoints, error in + if let error { + continuation.resume(throwing: error) + return + } + + guard let datagrams, let endpoints else { + continuation.resume(throwing: NEAppProxyFlowError(.aborted)) + return + } + + continuation.resume(returning: (datagrams, endpoints)) + } + } + } + + private func send(datagram: Data, through remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + remoteConnection.send(content: datagram, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + })) + } + } +} + +extension UDPFlowManager: Hashable { + static func == (lhs: UDPFlowManager, rhs: UDPFlowManager) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(flow) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift new file mode 100644 index 0000000000..12339a673d --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift @@ -0,0 +1,82 @@ +// +// TransparentProxyAppMessageHandler.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 OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// Handles app messages +/// +final class TransparentProxyAppMessageHandler { + + private let settings: TransparentProxySettings + + init(settings: TransparentProxySettings) { + self.settings = settings + } + + func handle(_ data: Data) async -> Data? { + do { + let message = try JSONDecoder().decode(TransparentProxyMessage.self, from: data) + return await handle(message) + } catch { + return nil + } + } + + /// Handles a message. + /// + /// This method will wrap the message into a request with a completion handler, and will process it. + /// The reason why this method wraps the message in a request is to ensure that the response + /// type stays syncrhonized between app and provider. + /// + private func handle(_ message: TransparentProxyMessage) async -> Data? { + await withCheckedContinuation { continuation in + var request: TransparentProxyRequest + + switch message { + case .changeSetting(let change): + request = .changeSetting(change, responseHandler: { + continuation.resume(returning: nil) + }) + } + + handle(request) + } + } + + /// Handles a request and calls the response handler when done. + /// + private func handle(_ request: TransparentProxyRequest) { + switch request { + case .changeSetting(let change, let responseHandler): + handle(change) + responseHandler() + } + } + + /// Handles a settings change. + /// + private func handle(_ settingChange: TransparentProxySettings.Change) { + switch settingChange { + case .appRoutingRules(let routingRules): + settings.appRoutingRules = routingRules + case .excludedDomains(let excludedDomains): + settings.excludedDomains = excludedDomains + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift new file mode 100644 index 0000000000..0881dba3b1 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift @@ -0,0 +1,67 @@ +// +// TransparentProxyRequest.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 NetworkExtension + +public enum TransparentProxyMessage: Codable { + case changeSetting(_ change: TransparentProxySettings.Change) +} + +/// A request for the TransparentProxyProvider. +/// +/// This enum associates a request with a response handler making XPC communication simpler. +/// Once the request completes, `responseHandler` will be called with the result. +/// +public enum TransparentProxyRequest { + case changeSetting(_ settingChange: TransparentProxySettings.Change, responseHandler: () -> Void) + + var message: TransparentProxyMessage { + switch self { + case .changeSetting(let change, _): + return .changeSetting(change) + } + } + + func handleResponse(data: Data?) { + switch self { + case .changeSetting(_, let handleResponse): + handleResponse() + } + } +} + +/// Respresents a transparent proxy session. +/// +/// Offers basic IPC communication support for the app that owns the proxy. This mechanism +/// is implemented through `NETunnelProviderSession` which means only the app that +/// owns the proxy can use this class. +/// +public class TransparentProxySession { + + private let session: NETunnelProviderSession + + init(_ session: NETunnelProviderSession) { + self.session = session + } + + func send(_ request: TransparentProxyRequest) throws { + let payload = try JSONEncoder().encode(request.message) + try session.sendProviderMessage(payload, responseHandler: request.handleResponse(data:)) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift new file mode 100644 index 0000000000..b4c9b8cebb --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift @@ -0,0 +1,89 @@ +// +// TransparentProxyControllerPixel.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 PixelKit + +extension TransparentProxyController.StartError: PixelKitEventErrorDetails { + public var underlyingError: Error? { + switch self { + case .failedToLoadConfiguration(let underlyingError), + .failedToSaveConfiguration(let underlyingError), + .failedToStartProvider(let underlyingError): + return underlyingError + default: + return nil + } + } +} + +extension TransparentProxyController { + + public enum Event: PixelKitEventV2 { + case startInitiated + case startSuccess + case startFailure(_ error: Error) + + // MARK: - PixelKit.Event + + public var name: String { + namePrefix + "_" + nameSuffix + } + + public var parameters: [String: String]? { + switch self { + case .startInitiated: + return nil + case .startSuccess: + return nil + case .startFailure: + return nil + } + } + + // MARK: - PixelKit Support + + private static let pixelNamePrefix = "vpn_proxy_controller" + + private var namePrefix: String { + Self.pixelNamePrefix + } + + private var nameSuffix: String { + switch self { + case .startInitiated: + return "start_initiated" + case .startFailure: + return "start_failure" + case .startSuccess: + return "start_success" + } + } + + public var error: Error? { + switch self { + case .startInitiated: + return nil + case .startFailure(let error): + return error + case .startSuccess: + return nil + } + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift new file mode 100644 index 0000000000..aff7421bea --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift @@ -0,0 +1,93 @@ +// +// TransparentProxyProviderPixel.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 PixelKit + +extension TransparentProxyProvider.StartError: ErrorWithPixelParameters { + public var errorParameters: [String: String] { + switch self { + case .failedToUpdateNetworkSettings(let underlyingError): + return [ + PixelKit.Parameters.underlyingErrorCode: "\((underlyingError as NSError).code)", + PixelKit.Parameters.underlyingErrorDesc: (underlyingError as NSError).domain, + ] + default: + return [:] + } + } +} + +extension TransparentProxyProvider { + + public enum Event: PixelKitEventV2 { + case failedToUpdateNetworkSettings(_ error: Error) + case startInitiated + case startSuccess + case startFailure(_ error: Error) + + private static let pixelNamePrefix = "vpn_proxy_provider" + + private var namePrefix: String { + Self.pixelNamePrefix + } + + private var namePostfix: String { + switch self { + case .failedToUpdateNetworkSettings: + return "failed_to_update_network_settings" + case .startFailure: + return "start_failure" + case .startInitiated: + return "start_initiated" + case .startSuccess: + return "start_success" + } + } + + public var name: String { + namePrefix + "_" + namePostfix + } + + public var parameters: [String: String]? { + switch self { + case.failedToUpdateNetworkSettings: + return nil + case .startFailure: + return nil + case .startInitiated: + return nil + case .startSuccess: + return nil + } + } + + public var error: Error? { + switch self { + case .failedToUpdateNetworkSettings(let error): + return error + case .startInitiated: + return nil + case .startFailure(let error): + return error + case .startSuccess: + return nil + } + } + } +} diff --git a/DuckDuckGoVPN/Bundle+Configuration.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift similarity index 56% rename from DuckDuckGoVPN/Bundle+Configuration.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift index 936c44a4a8..15d4a4e1a1 100644 --- a/DuckDuckGoVPN/Bundle+Configuration.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift @@ -1,7 +1,7 @@ // -// Bundle+Configuration.swift +// VPNAppRoutingRules.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// 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. @@ -18,14 +18,4 @@ import Foundation -extension Bundle { - private static let networkExtensionBundleIDKey = "SYSEX_BUNDLE_ID" - - var networkExtensionBundleID: String { - guard let bundleID = object(forInfoDictionaryKey: Self.networkExtensionBundleIDKey) as? String else { - fatalError("Info.plist is missing \(Self.networkExtensionBundleIDKey)") - } - - return bundleID - } -} +public typealias VPNAppRoutingRules = [String: VPNRoutingRule] diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift similarity index 59% rename from DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift index e90627cbc2..b96a0773cf 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift @@ -1,7 +1,7 @@ // -// NetworkProtectionExtensionMachService.swift +// VPNRoutingRule.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// 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. @@ -18,14 +18,12 @@ import Foundation -/// Helper methods associated with mach services. +/// Routing rules /// -final class NetworkProtectionExtensionMachService { - - /// Retrieves the mach service name from a network extension bundle. - /// - static func serviceName() -> String { - NetworkProtectionBundle.extensionBundle().machServiceName - } - +/// Note that there's no need for an `ignore` case because that's achieved by not having a rule +/// in the first place. +/// +public enum VPNRoutingRule: Codable, Equatable { + case block + case exclude } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift new file mode 100644 index 0000000000..db010ec2b5 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift @@ -0,0 +1,134 @@ +// +// TransparentProxySettings.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 + +public final class TransparentProxySettings { + public enum Change: Codable { + case appRoutingRules(_ routingRules: VPNAppRoutingRules) + case excludedDomains(_ excludedDomains: [String]) + } + + let defaults: UserDefaults + + private(set) public lazy var changePublisher: AnyPublisher = { + Publishers.MergeMany( + defaults.vpnProxyAppRoutingRulesPublisher + .dropFirst() + .removeDuplicates() + .map { routingRules in + Change.appRoutingRules(routingRules) + }.eraseToAnyPublisher(), + defaults.vpnProxyExcludedDomainsPublisher + .dropFirst() + .removeDuplicates() + .map { excludedDomains in + Change.excludedDomains(excludedDomains) + }.eraseToAnyPublisher() + ).eraseToAnyPublisher() + }() + + public init(defaults: UserDefaults) { + self.defaults = defaults + } + + // MARK: - Settings + + public var appRoutingRules: VPNAppRoutingRules { + get { + defaults.vpnProxyAppRoutingRules + } + + set { + defaults.vpnProxyAppRoutingRules = newValue + } + } + + public var excludedDomains: [String] { + get { + defaults.vpnProxyExcludedDomains + } + + set { + defaults.vpnProxyExcludedDomains = newValue + } + } + + // MARK: - Reset to factory defaults + + public func resetAll() { + defaults.resetVPNProxyAppRoutingRules() + defaults.resetVPNProxyExcludedDomains() + } + + // MARK: - App routing rules logic + + public func isBlocking(_ appIdentifier: String) -> Bool { + appRoutingRules[appIdentifier] == .block + } + + public func isExcluding(_ appIdentifier: String) -> Bool { + appRoutingRules[appIdentifier] == .exclude + } + + public func toggleBlocking(for appIdentifier: String) { + if isBlocking(appIdentifier) { + appRoutingRules.removeValue(forKey: appIdentifier) + } else { + appRoutingRules[appIdentifier] = .block + } + } + + public func toggleExclusion(for appIdentifier: String) { + if isExcluding(appIdentifier) { + appRoutingRules.removeValue(forKey: appIdentifier) + } else { + appRoutingRules[appIdentifier] = .exclude + } + } + + // MARK: - Snapshot support + + public func snapshot() -> TransparentProxySettingsSnapshot { + .init(appRoutingRules: appRoutingRules, excludedDomains: excludedDomains) + } + + public func apply(_ snapshot: TransparentProxySettingsSnapshot) { + appRoutingRules = snapshot.appRoutingRules + excludedDomains = snapshot.excludedDomains + } +} + +extension TransparentProxySettings: CustomStringConvertible { + public var description: String { + """ + TransparentProxySettings {\n + appRoutingRules: \(appRoutingRules)\n + excludedDomains: \(excludedDomains)\n + } + """ + } +} + +public struct TransparentProxySettingsSnapshot: Codable { + public static let key = "com.duckduckgo.TransparentProxySettingsSnapshot" + + public let appRoutingRules: VPNAppRoutingRules + public let excludedDomains: [String] +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift new file mode 100644 index 0000000000..1090ed1626 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift @@ -0,0 +1,79 @@ +// +// UserDefaults+excludedApps.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 + +extension UserDefaults { + private var vpnProxyAppRoutingRulesDataKey: String { + "vpnProxyAppRoutingRulesData" + } + + @objc + dynamic var vpnProxyAppRoutingRulesData: Data? { + get { + object(forKey: vpnProxyAppRoutingRulesDataKey) as? Data + } + + set { + guard let newValue, + newValue.count > 0 else { + + removeObject(forKey: vpnProxyAppRoutingRulesDataKey) + return + } + + set(newValue, forKey: vpnProxyAppRoutingRulesDataKey) + } + } + + var vpnProxyAppRoutingRules: VPNAppRoutingRules { + get { + guard let data = vpnProxyAppRoutingRulesData, + let routingRules = try? JSONDecoder().decode(VPNAppRoutingRules.self, from: data) else { + return [:] + } + + return routingRules + } + + set { + if newValue.isEmpty { + vpnProxyAppRoutingRulesData = nil + return + } + + guard let data = try? JSONEncoder().encode(newValue) else { + vpnProxyAppRoutingRulesData = nil + return + } + + vpnProxyAppRoutingRulesData = data + } + } + + var vpnProxyAppRoutingRulesPublisher: AnyPublisher { + publisher(for: \.vpnProxyAppRoutingRulesData).map { [weak self] _ in + self?.vpnProxyAppRoutingRules ?? [:] + }.eraseToAnyPublisher() + } + + func resetVPNProxyAppRoutingRules() { + removeObject(forKey: vpnProxyAppRoutingRulesDataKey) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift new file mode 100644 index 0000000000..7500178da7 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift @@ -0,0 +1,51 @@ +// +// UserDefaults+excludedDomains.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 + +extension UserDefaults { + private var vpnProxyExcludedDomainsKey: String { + "vpnProxyExcludedDomains" + } + + @objc + dynamic var vpnProxyExcludedDomains: [String] { + get { + object(forKey: vpnProxyExcludedDomainsKey) as? [String] ?? [] + } + + set { + guard newValue.count > 0 else { + + removeObject(forKey: vpnProxyExcludedDomainsKey) + return + } + + set(newValue, forKey: vpnProxyExcludedDomainsKey) + } + } + + var vpnProxyExcludedDomainsPublisher: AnyPublisher<[String], Never> { + publisher(for: \.vpnProxyExcludedDomains).eraseToAnyPublisher() + } + + func resetVPNProxyExcludedDomains() { + removeObject(forKey: vpnProxyExcludedDomainsKey) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift new file mode 100644 index 0000000000..fdc7fb3177 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift @@ -0,0 +1,293 @@ +// +// TransparentProxyController.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 NetworkProtection +import OSLog // swiftlint:disable:this enforce_os_log_wrapper +import PixelKit +import SystemExtensions + +/// Controller for ``TransparentProxyProvider`` +/// +@MainActor +public final class TransparentProxyController { + + public enum StartError: Error { + case attemptToStartWithoutBackingActiveFeatures + case couldNotRetrieveProtocolConfiguration + case couldNotEncodeSettingsSnapshot + case failedToLoadConfiguration(_ error: Error) + case failedToSaveConfiguration(_ error: Error) + case failedToStartProvider(_ error: Error) + } + + public typealias EventCallback = (Event) -> Void + public typealias ManagerSetupCallback = (_ manager: NETransparentProxyManager) async -> Void + + /// Dry mode means this won't really do anything to start or stop the proxy. + /// + /// This is useful for testing. + /// + private let dryMode: Bool + + /// The bundleID of the extension that contains the ``TransparentProxyProvider``. + /// + private let extensionID: String + + /// The event handler + /// + public var eventHandler: EventCallback? + + /// Callback to set up a ``NETransparentProxyManager``. + /// + public let setup: ManagerSetupCallback + + private var internalManager: NETransparentProxyManager? + + /// Whether the proxy settings should be stored in the provider configuration. + /// + /// We recommend setting this to true if the provider is running in a System Extension and can't access + /// shared `TransparentProxySettings`. If the provider is in an App Extension you should instead + /// use a shared `TransparentProxySettings` and set this to false. + /// + private let storeSettingsInProviderConfiguration: Bool + public let settings: TransparentProxySettings + private let notificationCenter: NotificationCenter + private var cancellables = Set() + + // MARK: - Initializers + + /// Default initializer. + /// + /// - Parameters: + /// - extensionID: the bundleID for the extension that contains the ``TransparentProxyProvider``. + /// This class DOES NOT take any responsibility in installing the system extension. It only uses + /// the extensionID to identify the appropriate manager configuration to load / save. + /// - storeSettingsInProviderConfiguration: whether the provider configuration will be used for storing + /// the proxy settings. Should be `true` when using a System Extension and `false` when using + /// an App Extension. + /// - settings: the settings to use for this proxy. + /// - dryMode: whether this class is initialized in dry mode. + /// - setup: a callback that will be called whenever a ``NETransparentProxyManager`` needs + /// to be setup. + /// + public init(extensionID: String, + storeSettingsInProviderConfiguration: Bool, + settings: TransparentProxySettings, + notificationCenter: NotificationCenter = .default, + dryMode: Bool = false, + setup: @escaping ManagerSetupCallback) { + + self.dryMode = dryMode + self.extensionID = extensionID + self.notificationCenter = notificationCenter + self.settings = settings + self.setup = setup + self.storeSettingsInProviderConfiguration = storeSettingsInProviderConfiguration + + subscribeToProviderConfigurationChanges() + subscribeToSettingsChanges() + } + + // MARK: - Relay Settings Changes + + private func subscribeToProviderConfigurationChanges() { + notificationCenter.publisher(for: .NEVPNConfigurationChange) + .receive(on: DispatchQueue.main) + .sink { _ in + self.reloadProviderConfiguration() + } + .store(in: &cancellables) + } + + private func reloadProviderConfiguration() { + Task { @MainActor in + try? await self.manager?.loadFromPreferences() + } + } + + private func subscribeToSettingsChanges() { + settings.changePublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: relay(_:)) + .store(in: &cancellables) + } + + private func relay(_ change: TransparentProxySettings.Change) { + Task { @MainActor in + guard let session = await session else { + return + } + + switch session.status { + case .connected, .connecting, .reasserting: + break + default: + return + } + + try TransparentProxySession(session).send(.changeSetting(change, responseHandler: { + // no-op + })) + } + } + + // MARK: - Setting up NETransparentProxyManager + + /// Loads a saved manager + /// + /// This is a bit of a hack that will be run just once for the instance. The reason we want this to run only once is that + /// `NETransparentProxyManager.loadAllFromPreferences()` has a bug where it triggers status change + /// notifications. If the code trying to retrieve the manager is the result of a notification, we may soon find outselves + /// in an infinite loop. + /// + private var triedLoadingManager = false + + /// Loads the configuration matching our ``extensionID``. + /// + public var manager: NETransparentProxyManager? { + get async { + if let internalManager { + return internalManager + } + + if !triedLoadingManager { + triedLoadingManager = true + + let manager = try? await NETransparentProxyManager.loadAllFromPreferences().first { manager in + (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == extensionID + } + self.internalManager = manager + } + + return internalManager + } + } + + /// Loads an existing configuration or creates a new one, if one doesn't exist. + /// + /// - Returns a properly configured `NETransparentProxyManager`. + /// + public func loadOrCreateConfiguration() async throws -> NETransparentProxyManager { + let manager = await manager ?? { + let manager = NETransparentProxyManager() + internalManager = manager + return manager + }() + + await setup(manager) + try setupAdditionalProviderConfiguration(manager) + + try await manager.saveToPreferences() + try await manager.loadFromPreferences() + + return manager + } + + private func setupAdditionalProviderConfiguration(_ manager: NETransparentProxyManager) throws { + guard storeSettingsInProviderConfiguration else { + return + } + + guard let providerProtocol = manager.protocolConfiguration as? NETunnelProviderProtocol else { + throw StartError.couldNotRetrieveProtocolConfiguration + } + + var providerConfiguration = providerProtocol.providerConfiguration ?? [String: Any]() + + guard let encodedSettings = try? JSONEncoder().encode(settings.snapshot()), + let encodedSettingsString = String(data: encodedSettings, encoding: .utf8) else { + + throw StartError.couldNotEncodeSettingsSnapshot + } + + providerConfiguration[TransparentProxySettingsSnapshot.key] = encodedSettingsString as NSString + providerProtocol.providerConfiguration = providerConfiguration + + } + + // MARK: - Connection & Session + + public var connection: NEVPNConnection? { + get async { + await manager?.connection + } + } + + public var session: NETunnelProviderSession? { + get async { + guard let manager = await manager, + let session = manager.connection as? NETunnelProviderSession else { + + // The active connection is not running, so there's no session, this is acceptable + return nil + } + + return session + } + } + + // MARK: - Connection + + public var status: NEVPNStatus { + get async { + await connection?.status ?? .disconnected + } + } + + // MARK: - Start & stop the proxy + + public var isRequiredForActiveFeatures: Bool { + settings.appRoutingRules.count > 0 || settings.excludedDomains.count > 0 + } + + public func start() async throws { + eventHandler?(.startInitiated) + + guard isRequiredForActiveFeatures else { + let error = StartError.attemptToStartWithoutBackingActiveFeatures + eventHandler?(.startFailure(error)) + throw error + } + + let manager: NETransparentProxyManager + + do { + manager = try await loadOrCreateConfiguration() + } catch { + eventHandler?(.startFailure(error)) + throw error + } + + do { + try manager.connection.startVPNTunnel(options: [:]) + } catch { + let error = StartError.failedToStartProvider(error) + eventHandler?(.startFailure(error)) + throw error + } + + eventHandler?(.startSuccess) + } + + public func stop() async { + await connection?.stopVPNTunnel() + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift new file mode 100644 index 0000000000..33b75fd73b --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift @@ -0,0 +1,389 @@ +// +// TransparentProxyProvider.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 NetworkExtension +import NetworkProtection +import os.log // swiftlint:disable:this enforce_os_log_wrapper +import SystemConfiguration + +open class TransparentProxyProvider: NETransparentProxyProvider { + + public enum StartError: Error { + case missingProviderConfiguration + case failedToUpdateNetworkSettings(underlyingError: Error) + } + + public typealias EventCallback = (Event) -> Void + public typealias LoadOptionsCallback = (_ options: [String: Any]?) throws -> Void + + static let dnsPort = 53 + + @TCPFlowActor + var tcpFlowManagers = Set() + + @UDPFlowActor + var udpFlowManagers = Set() + + private let monitor = nw_path_monitor_create() + var directInterface: nw_interface_t? + + private let bMonitor = NWPathMonitor() + var interface: NWInterface? + + public let configuration: Configuration + public let settings: TransparentProxySettings + + @MainActor + public var isRunning = false + + public var eventHandler: EventCallback? + private let logger: Logger + + private lazy var appMessageHandler = TransparentProxyAppMessageHandler(settings: settings) + + // MARK: - Init + + public init(settings: TransparentProxySettings, + configuration: Configuration, + logger: Logger) { + + self.configuration = configuration + self.logger = logger + self.settings = settings + + logger.debug("[+] \(String(describing: Self.self), privacy: .public)") + } + + deinit { + logger.debug("[-] \(String(describing: Self.self), privacy: .public)") + } + + private func loadProviderConfiguration() throws { + guard configuration.loadSettingsFromProviderConfiguration else { + return + } + + guard let providerConfiguration = (protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration, + let encodedSettingsString = providerConfiguration[TransparentProxySettingsSnapshot.key] as? String, + let encodedSettings = encodedSettingsString.data(using: .utf8) else { + + throw StartError.missingProviderConfiguration + } + + let snapshot = try JSONDecoder().decode(TransparentProxySettingsSnapshot.self, from: encodedSettings) + settings.apply(snapshot) + } + + @MainActor + public func updateNetworkSettings() async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @MainActor in + let networkSettings = makeNetworkSettings() + logger.log("Updating network settings: \(String(describing: networkSettings), privacy: .public)") + + setTunnelNetworkSettings(networkSettings) { [eventHandler, logger] error in + if let error { + logger.error("Failed to update network settings: \(String(describing: error), privacy: .public)") + eventHandler?(.failedToUpdateNetworkSettings(error)) + continuation.resume(throwing: error) + return + } + + logger.log("Successfully Updated network settings: \(String(describing: error), privacy: .public))") + continuation.resume() + } + } + } + } + + private func makeNetworkSettings() -> NETransparentProxyNetworkSettings { + let networkSettings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + + networkSettings.includedNetworkRules = [ + NENetworkRule(remoteNetwork: NWHostEndpoint(hostname: "127.0.0.1", port: ""), remotePrefix: 0, localNetwork: nil, localPrefix: 0, protocol: .any, direction: .outbound) + ] + + return networkSettings + } + + override public func startProxy(options: [String: Any]?, + completionHandler: @escaping (Error?) -> Void) { + + eventHandler?(.startInitiated) + + logger.log( + """ + Starting proxy\n + > configuration: \(String(describing: self.configuration), privacy: .public)\n + > settings: \(String(describing: self.settings), privacy: .public)\n + > options: \(String(describing: options), privacy: .public) + """) + + do { + try loadProviderConfiguration() + } catch { + logger.error("Failed to load provider configuration, bailing out") + eventHandler?(.startFailure(error)) + completionHandler(error) + return + } + + Task { @MainActor in + do { + startMonitoringNetworkInterfaces() + + try await updateNetworkSettings() + logger.log("Proxy started successfully") + isRunning = true + eventHandler?(.startSuccess) + completionHandler(nil) + } catch { + let error = StartError.failedToUpdateNetworkSettings(underlyingError: error) + logger.error("Proxy failed to start \(String(reflecting: error), privacy: .public)") + eventHandler?(.startFailure(error)) + completionHandler(error) + } + } + } + + override public func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + logger.log("Stopping proxy with reason: \(String(reflecting: reason), privacy: .public)") + + Task { @MainActor in + stopMonitoringNetworkInterfaces() + isRunning = false + completionHandler() + } + } + + override public func sleep(completionHandler: @escaping () -> Void) { + Task { @MainActor in + stopMonitoringNetworkInterfaces() + logger.log("The proxy is now sleeping") + completionHandler() + } + } + + override public func wake() { + Task { @MainActor in + logger.log("The proxy is now awake") + startMonitoringNetworkInterfaces() + } + } + + override public func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool { + guard let flow = flow as? NEAppProxyTCPFlow else { + logger.info("Expected a TCP flow, but got something else. We're ignoring it.") + return false + } + + guard let remoteEndpoint = flow.remoteEndpoint as? NWHostEndpoint, + !isDnsServer(remoteEndpoint) else { + return false + } + + let printableRemote = flow.remoteHostname ?? (flow.remoteEndpoint as? NWHostEndpoint)?.hostname ?? "unknown" + + logger.debug( + """ + [TCP] New flow: \(String(describing: flow), privacy: .public) + - remote: \(printableRemote, privacy: .public) + - flowID: \(String(describing: flow.metaData.filterFlowIdentifier?.uuidString), privacy: .public) + - appID: \(String(describing: flow.metaData.sourceAppSigningIdentifier), privacy: .public) + """) + + guard let interface else { + logger.error("[TCP: \(String(describing: flow), privacy: .public)] Expected an interface to exclude traffic through") + return false + } + + switch path(for: flow) { + case .block(let reason): + switch reason { + case .appRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Blocking traffic due to app rule") + case .domainRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Blocking traffic due to domain rule") + } + case .excludeFromVPN(let reason): + switch reason { + case .appRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Excluding traffic due to app rule") + case .domainRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Excluding traffic due to domain rule") + } + case .routeThroughVPN: + return false + } + + flow.networkInterface = directInterface + + Task { @TCPFlowActor in + let flowManager = TCPFlowManager(flow: flow) + tcpFlowManagers.insert(flowManager) + + try? await flowManager.start(interface: interface) + tcpFlowManagers.remove(flowManager) + } + + return true + } + + override public func handleNewUDPFlow(_ flow: NEAppProxyUDPFlow, initialRemoteEndpoint remoteEndpoint: NWEndpoint) -> Bool { + + guard let remoteEndpoint = remoteEndpoint as? NWHostEndpoint, + !isDnsServer(remoteEndpoint) else { + return false + } + + let printableRemote = remoteEndpoint.hostname + + logger.log( + """ + [UDP] New flow: \(String(describing: flow), privacy: .public) + - remote: \(printableRemote, privacy: .public) + - flowID: \(String(describing: flow.metaData.filterFlowIdentifier?.uuidString), privacy: .public) + - appID: \(String(describing: flow.metaData.sourceAppSigningIdentifier), privacy: .public) + """) + + guard let interface else { + logger.error("[UDP: \(String(describing: flow), privacy: .public)] Expected an interface to exclude traffic through") + return false + } + + switch path(for: flow) { + case .block(let reason): + switch reason { + case .appRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Blocking traffic due to app rule") + case .domainRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Blocking traffic due to domain rule") + } + case .excludeFromVPN(let reason): + switch reason { + case .appRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Excluding traffic due to app rule") + case .domainRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Excluding traffic due to domain rule") + } + case .routeThroughVPN: + return false + } + + flow.networkInterface = directInterface + + Task { @UDPFlowActor in + let flowManager = UDPFlowManager(flow: flow) + udpFlowManagers.insert(flowManager) + + try? await flowManager.start(interface: interface) + udpFlowManagers.remove(flowManager) + } + + return true + } + + // MARK: - Path Monitors + + @MainActor + private func startMonitoringNetworkInterfaces() { + bMonitor.pathUpdateHandler = { [weak self, logger] path in + logger.log("Available interfaces updated: \(String(reflecting: path.availableInterfaces), privacy: .public)") + + self?.interface = path.availableInterfaces.first { interface in + interface.type != .other + } + } + bMonitor.start(queue: .main) + + nw_path_monitor_set_queue(monitor, .main) + nw_path_monitor_set_update_handler(monitor) { [weak self, logger] path in + guard let self else { return } + + let interfaces = SCNetworkInterfaceCopyAll() + logger.log("Available interfaces updated: \(String(reflecting: interfaces), privacy: .public)") + + nw_path_enumerate_interfaces(path) { interface in + guard nw_interface_get_type(interface) != nw_interface_type_other else { + return true + } + + self.directInterface = interface + return false + } + } + + nw_path_monitor_start(monitor) + } + + @MainActor + private func stopMonitoringNetworkInterfaces() { + bMonitor.cancel() + nw_path_monitor_cancel(monitor) + } + + // MARK: - Ignoring DNS flows + + private func isDnsServer(_ endpoint: NWHostEndpoint) -> Bool { + Int(endpoint.port) == Self.dnsPort + } + + // MARK: - VPN exclusions logic + + private enum FlowPath { + case block(dueTo: Reason) + case excludeFromVPN(dueTo: Reason) + case routeThroughVPN + + enum Reason { + case appRule + case domainRule + } + } + + private func path(for flow: NEAppProxyFlow) -> FlowPath { + let appIdentifier = flow.metaData.sourceAppSigningIdentifier + + switch settings.appRoutingRules[appIdentifier] { + case .none: + if let hostname = flow.remoteHostname, + isExcludedDomain(hostname) { + return .excludeFromVPN(dueTo: .domainRule) + } + + return .routeThroughVPN + case .block: + return .block(dueTo: .appRule) + case .exclude: + return .excludeFromVPN(dueTo: .domainRule) + } + } + + private func isExcludedDomain(_ hostname: String) -> Bool { + settings.excludedDomains.contains { excludedDomain in + hostname.hasSuffix(excludedDomain) + } + } + + // MARK: - Communication with App + + override public func handleAppMessage(_ messageData: Data) async -> Data? { + await appMessageHandler.handle(messageData) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift new file mode 100644 index 0000000000..3e841faeca --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift @@ -0,0 +1,40 @@ +// +// TransparentProxyProviderConfiguration.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 + +extension TransparentProxyProvider { + /// Configuration to define behaviour for the provider based on the parent process' + /// business domain. + /// + /// This should not be passed in the startup options dictionary. + /// + public struct Configuration { + /// Whether the proxy settings should be loaded from the provider configuration in the startup options. + /// + /// We recommend setting this to true if the provider is running in a System Extension and can't access + /// shared `TransparentProxySettings`. If the provider is in an App Extension you should instead + /// use a shared `TransparentProxySettings` and set this to false. + /// + let loadSettingsFromProviderConfiguration: Bool + + public init(loadSettingsFromProviderConfiguration: Bool) { + self.loadSettingsFromProviderConfiguration = loadSettingsFromProviderConfiguration + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index 66f9bb15ea..2575803866 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -54,9 +54,11 @@ public struct NetworkProtectionStatusView: View { PromptActionView(model: promptActionViewModel) .padding(.horizontal, 5) .padding(.top, 5) + .transition(.slide) } else { if let healthWarning = model.issueDescription { connectionHealthWarningView(message: healthWarning) + .transition(.slide) } } @@ -67,12 +69,14 @@ public struct NetworkProtectionStatusView: View { if model.showDebugInformation { DebugInformationView(model: DebugInformationViewModel()) + .transition(.slide) } bottomMenuView() } .padding(5) .frame(maxWidth: 350, alignment: .top) + .transition(.slide) } // MARK: - Composite Views diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 2a86e999d0..754ca81034 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -143,9 +143,9 @@ extension NetworkProtectionStatusView { onboardingStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] status in - self?.onboardingStatus = status - } - .store(in: &cancellables) + self?.onboardingStatus = status + } + .store(in: &cancellables) } func refreshLoginItemStatus() { @@ -184,14 +184,14 @@ extension NetworkProtectionStatusView { .subscribe(on: Self.tunnelErrorDispatchQueue) .sink { [weak self] errorMessage in - guard let self else { - return - } + guard let self else { + return + } - Task { @MainActor in - self.lastTunnelErrorMessage = errorMessage - } - }.store(in: &cancellables) + Task { @MainActor in + self.lastTunnelErrorMessage = errorMessage + } + }.store(in: &cancellables) } private func subscribeToControllerErrorMessages() { @@ -199,14 +199,14 @@ extension NetworkProtectionStatusView { .subscribe(on: Self.controllerErrorDispatchQueue) .sink { [weak self] errorMessage in - guard let self else { - return - } + guard let self else { + return + } - Task { @MainActor in - self.lastControllerErrorMessage = errorMessage - } - }.store(in: &cancellables) + Task { @MainActor in + self.lastControllerErrorMessage = errorMessage + } + }.store(in: &cancellables) } private func subscribeToDebugInformationChanges() { diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift new file mode 100644 index 0000000000..bd21fe50d1 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift @@ -0,0 +1,120 @@ +// +// TransparentProxyControllerPixelTests.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 +@testable import NetworkProtectionProxy +import PixelKit +import PixelKitTestingUtilities +import XCTest + +extension TransparentProxyController.Event: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyController.Event, rhs: NetworkProtectionProxy.TransparentProxyController.Event) -> Bool { + + lhs.name == rhs.name && lhs.parameters == rhs.parameters + } + + public func hash(into hasher: inout Hasher) { + name.hash(into: &hasher) + parameters.hash(into: &hasher) + } +} + +extension TransparentProxyController.StartError: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyController.StartError, rhs: NetworkProtectionProxy.TransparentProxyController.StartError) -> Bool { + + let lhs = lhs as NSError + let rhs = rhs as NSError + + return lhs.code == rhs.code && lhs.domain == rhs.domain + } + + public func hash(into hasher: inout Hasher) { + (self as NSError).hash(into: &hasher) + (underlyingError as? NSError)?.hash(into: &hasher) + } +} + +final class TransparentProxyControllerPixelTests: XCTestCase { + + static let startFailureFullPixelName = "m_mac_vpn_proxy_controller_start_failure" + static let startInitiatedFullPixelName = "m_mac_vpn_proxy_controller_start_initiated" + static let startSuccessFullPixelName = "m_mac_vpn_proxy_controller_start_success" + + enum TestError: PixelKitEventErrorDetails { + case testError + + static let underlyingError = NSError(domain: "test", code: 1) + + var underlyingError: Error? { + Self.underlyingError + } + } + + // MARK: - Test Firing Pixels + + func testFiringPixelsWithoutParameters() { + let tests: [TransparentProxyController.Event: PixelFireExpectations] = [ + .startInitiated: PixelFireExpectations(pixelName: Self.startInitiatedFullPixelName), + .startSuccess: PixelFireExpectations(pixelName: Self.startSuccessFullPixelName) + ] + + for (event, expectations) in tests { + verifyThat(event, + meets: expectations, + file: #filePath, + line: #line) + } + } + + func testFiringStartFailures() { + // Just a convenience method to return the right expectation for each error + func expectaton(forError error: TransparentProxyController.StartError) -> PixelFireExpectations { + switch error { + case .attemptToStartWithoutBackingActiveFeatures, + .couldNotEncodeSettingsSnapshot, + .couldNotRetrieveProtocolConfiguration: + return PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: error) + case .failedToLoadConfiguration(let underlyingError), + .failedToSaveConfiguration(let underlyingError), + .failedToStartProvider(let underlyingError): + return PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: error, + underlyingError: underlyingError) + } + } + + let errors: [TransparentProxyController.StartError] = [ + .attemptToStartWithoutBackingActiveFeatures, + .couldNotEncodeSettingsSnapshot, + .couldNotRetrieveProtocolConfiguration, + .failedToLoadConfiguration(TestError.underlyingError), + .failedToSaveConfiguration(TestError.underlyingError), + .failedToStartProvider(TestError.underlyingError) + ] + + for error in errors { + verifyThat(TransparentProxyController.Event.startFailure(error), + meets: expectaton(forError: error), + file: #filePath, + line: #line) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift new file mode 100644 index 0000000000..faf31729a4 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift @@ -0,0 +1,66 @@ +// +// TransparentProxyProviderPixelTests.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 +@testable import NetworkProtectionProxy +import PixelKit +import PixelKitTestingUtilities +import XCTest + +extension TransparentProxyProvider.Event: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyProvider.Event, rhs: NetworkProtectionProxy.TransparentProxyProvider.Event) -> Bool { + + lhs.name == rhs.name && lhs.parameters == rhs.parameters + } + + public func hash(into hasher: inout Hasher) { + name.hash(into: &hasher) + parameters.hash(into: &hasher) + } +} + +final class TransparentProxyProviderPixelTests: XCTestCase { + + static let startFailureFullPixelName = "m_mac_vpn_proxy_provider_start_failure" + static let startInitiatedFullPixelName = "m_mac_vpn_proxy_provider_start_initiated" + static let startSuccessFullPixelName = "m_mac_vpn_proxy_provider_start_success" + + enum TestError: Error { + case testError + } + + // MARK: - Test Firing Pixels + + func testFiringPixels() { + let tests: [TransparentProxyProvider.Event: PixelFireExpectations] = [ + .startInitiated: PixelFireExpectations(pixelName: Self.startInitiatedFullPixelName), + .startFailure(TestError.testError): + PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: TestError.testError), + .startSuccess: PixelFireExpectations(pixelName: Self.startSuccessFullPixelName) + ] + + for (event, expectations) in tests { + verifyThat(event, + meets: expectations, + file: #filePath, + line: #line) + } + } +} diff --git a/LocalPackages/PixelKit/Package.swift b/LocalPackages/PixelKit/Package.swift index 61de75cd32..1222931baa 100644 --- a/LocalPackages/PixelKit/Package.swift +++ b/LocalPackages/PixelKit/Package.swift @@ -20,7 +20,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift index ffd9d88796..7a25f133ff 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift @@ -94,11 +94,13 @@ public extension Error { let nsError = self as NSError params[PixelKit.Parameters.errorCode] = "\(nsError.code)" - params[PixelKit.Parameters.errorDesc] = nsError.domain + params[PixelKit.Parameters.errorDomain] = nsError.domain + params[PixelKit.Parameters.errorDesc] = nsError.localizedDescription if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { params[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.domain + params[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain + params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.localizedDescription } if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift index b13aea7b17..9c616d0060 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift @@ -275,11 +275,23 @@ public final class PixelKit { newParams = nil } + let newError: Error? + + if let event = event as? PixelKitEventV2 { + // For v2 events we only consider the error specified in the event + // and purposedly ignore the parameter in this call. + // This is to encourage moving the error over to the protocol error + // instead of still relying on the parameter of this call. + newError = event.error + } else { + newError = error + } + fire(pixelNamed: pixelName, frequency: frequency, withHeaders: headers, withAdditionalParameters: newParams, - withError: error, + withError: newError, allowedQueryReservedCharacters: allowedQueryReservedCharacters, includeAppVersionParameter: includeAppVersionParameter, onComplete: onComplete) @@ -365,8 +377,16 @@ extension Dictionary where Key == String, Value == String { self[PixelKit.Parameters.errorCode] = "\(nsError.code)" self[PixelKit.Parameters.errorDomain] = nsError.domain + self[PixelKit.Parameters.errorDesc] = nsError.localizedDescription + + if let error = error as? PixelKitEventErrorDetails, + let underlyingError = error.underlyingError { - if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { + let underlyingNSError = underlyingError as NSError + self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" + self[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain + self[PixelKit.Parameters.underlyingErrorDesc] = underlyingNSError.localizedDescription + } else if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" self[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain } else if let sqlErrorCode = nsError.userInfo["NSSQLiteErrorDomain"] as? NSNumber { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift index 83965ba999..bc87070df3 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift @@ -34,7 +34,7 @@ public final class DebugEvent: PixelKitEvent { } public let eventType: EventType - private let error: Error? + public let error: Error? public init(eventType: EventType, error: Error? = nil) { self.eventType = eventType diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift new file mode 100644 index 0000000000..7048519e32 --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift @@ -0,0 +1,70 @@ +// +// PixelKitEventV2.swift +// +// Copyright © 2023 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 PixelKitEventErrorDetails: Error { + var underlyingError: Error? { get } +} + +extension PixelKitEventErrorDetails { + var underlyingErrorParameters: [String: String] { + guard let nsError = underlyingError as? NSError else { + return [:] + } + + return [ + PixelKit.Parameters.underlyingErrorCode: "\(nsError.code)", + PixelKit.Parameters.underlyingErrorDomain: nsError.domain, + PixelKit.Parameters.underlyingErrorDesc: nsError.localizedDescription + ] + } +} + +/// New version of this protocol that allows us to maintain backwards-compatibility with PixelKitEvent +/// +/// This new implementation seeks to unify the handling of standard pixel parameters inside PixelKit. +/// The starting example of how this can be useful is error parameter handling - this protocol allows +/// the implementer to speciy an error without having to know about the parametrization of the error. +/// +/// The reason this wasn't done directly in `PixelKitEvent` is to reduce the risk of breaking existing +/// pixels, and to allow us to migrate towards this incrementally. +/// +public protocol PixelKitEventV2: PixelKitEvent { + var error: Error? { get } +} + +extension PixelKitEventV2 { + var pixelParameters: [String: String] { + guard let error else { + return [:] + } + + let nsError = error as NSError + var parameters = [ + PixelKit.Parameters.errorCode: "\(nsError.code)", + PixelKit.Parameters.errorDomain: nsError.domain, + ] + + if let error = error as? PixelKitEventErrorDetails { + parameters.merge(error.underlyingErrorParameters, uniquingKeysWith: { $1 }) + } + + return parameters + } +} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift new file mode 100644 index 0000000000..067eee091e --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift @@ -0,0 +1,36 @@ +// +// PixelFireExpectations.swift +// +// Copyright © 2023 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 + +/// Structure containing information about a pixel fire event. +/// +/// This is useful for test validation for libraries that rely on PixelKit, to make sure the pixels contain +/// all of the fields they are supposed to contain.. +/// +public struct PixelFireExpectations { + let pixelName: String + var error: Error? + var underlyingError: Error? + + public init(pixelName: String, error: Error? = nil, underlyingError: Error? = nil) { + self.pixelName = pixelName + self.error = error + self.underlyingError = underlyingError + } +} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift new file mode 100644 index 0000000000..5088ba1371 --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift @@ -0,0 +1,148 @@ +// +// XCTestCase+PixelKit.swift +// +// Copyright © 2023 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 +@testable import PixelKit +import XCTest + +public extension XCTestCase { + + // MARK: - Parameters + + /// List of standard pixel parameters. + /// + /// This is useful to support filtering these parameters out if needed. + /// + private static var standardPixelParameters = [ + PixelKit.Parameters.appVersion, + PixelKit.Parameters.pixelSource, + PixelKit.Parameters.test + ] + + /// List of errror pixel parameters + /// + private static var errorPixelParameters = [ + PixelKit.Parameters.errorCode, + PixelKit.Parameters.errorDomain, + PixelKit.Parameters.errorDesc + ] + + /// List of underlying error pixel parameters + /// + private static var underlyingErrorPixelParameters = [ + PixelKit.Parameters.underlyingErrorCode, + PixelKit.Parameters.underlyingErrorDomain, + PixelKit.Parameters.underlyingErrorDesc + ] + + /// Filter out the standard parameters. + /// + private static func filterStandardPixelParameters(from parameters: [String: String]) -> [String: String] { + parameters.filter { element in + !standardPixelParameters.contains(element.key) + } + } + + static var pixelPlatformPrefix: String { +#if os(macOS) + return "m_mac_" +#else + // Intentionally left blank for now because PixelKit currently doesn't support + // other platforms, but if we decide to implement another platform this'll fail + // and indicate that we need a value here. +#endif + } + + func expectedParameters(for event: PixelKitEventV2) -> [String: String] { + var expectedParameters = [String: String]() + + if let error = event.error { + let nsError = error as NSError + expectedParameters[PixelKit.Parameters.errorCode] = "\(nsError.code)" + expectedParameters[PixelKit.Parameters.errorDomain] = nsError.domain + expectedParameters[PixelKit.Parameters.errorDesc] = nsError.localizedDescription + + if let underlyingError = (error as? PixelKitEventErrorDetails)?.underlyingError { + let underlyingNSError = underlyingError as NSError + expectedParameters[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" + expectedParameters[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain + expectedParameters[PixelKit.Parameters.underlyingErrorDesc] = underlyingNSError.localizedDescription + } + } + + return expectedParameters + } + + // MARK: - Misc Convenience + + private var userDefaults: UserDefaults { + UserDefaults(suiteName: "testing_\(UUID().uuidString)")! + } + + // MARK: - Pixel Firing Expectations + + /// Provides some snapshot of a fired pixel so that external libraries can validate all the expected info is included. + /// + /// This method also checks that there is internal consistency in the expected fields. + /// + func verifyThat(_ event: PixelKitEventV2, meets expectations: PixelFireExpectations, file: StaticString, line: UInt) { + + let expectedPixelName = Self.pixelPlatformPrefix + event.name + let expectedParameters = expectedParameters(for: event) + let callbackExecutedExpectation = expectation(description: "The PixelKit callback has been executed") + + PixelKit.setUp(dryRun: false, + appVersion: "1.0.5", + source: "test-app", + defaultHeaders: [:], + log: .disabled, + defaults: userDefaults) { firedPixelName, _, firedParameters, _, _, completion in + callbackExecutedExpectation.fulfill() + + let firedParameters = Self.filterStandardPixelParameters(from: firedParameters) + + // Internal validations + + XCTAssertEqual(firedPixelName, expectedPixelName, file: file, line: line) + XCTAssertEqual(firedParameters, expectedParameters, file: file, line: line) + + // Expectations + + XCTAssertEqual(firedPixelName, expectations.pixelName) + + if let error = expectations.error { + let nsError = error as NSError + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorCode], String(nsError.code), file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDomain], nsError.domain, file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDesc], nsError.localizedDescription, file: file, line: line) + } + + if let underlyingError = expectations.underlyingError { + let nsError = underlyingError as NSError + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorCode], String(nsError.code), file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDomain], nsError.domain, file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDesc], nsError.localizedDescription, file: file, line: line) + } + + completion(true, nil) + } + + PixelKit.fire(event) + waitForExpectations(timeout: 0.1) + } +} diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 197a2c4eee..e5192fc149 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SwiftUIExtensions/Package.swift b/LocalPackages/SwiftUIExtensions/Package.swift index 6f8c716bc3..10d667a750 100644 --- a/LocalPackages/SwiftUIExtensions/Package.swift +++ b/LocalPackages/SwiftUIExtensions/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library(name: "PreferencesViews", targets: ["PreferencesViews"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 678077f716..6da8332d6a 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(path: "../SwiftUIExtensions"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 188a15c3ad..43bafef378 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -16,7 +16,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/XPCHelper/Package.swift b/LocalPackages/XPCHelper/Package.swift index fb49a79f25..e62141ec7a 100644 --- a/LocalPackages/XPCHelper/Package.swift +++ b/LocalPackages/XPCHelper/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "XPCHelper", targets: ["XPCHelper"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/NetworkProtectionSystemExtension/Info.plist b/NetworkProtectionSystemExtension/Info.plist index c35c8be1ca..43844d5702 100644 --- a/NetworkProtectionSystemExtension/Info.plist +++ b/NetworkProtectionSystemExtension/Info.plist @@ -14,6 +14,8 @@ com.apple.networkextension.packet-tunnel $(PRODUCT_MODULE_NAME).MacPacketTunnelProvider + com.apple.networkextension.app-proxy + $(PRODUCT_MODULE_NAME).MacTransparentProxyProvider MAIN_BUNDLE_IDENTIFIER $(MAIN_BUNDLE_IDENTIFIER) diff --git a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements index a049fa6886..4252e67c8e 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements +++ b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.security.app-sandbox diff --git a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements index f7d87546d2..23068f001f 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements +++ b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.security.app-sandbox diff --git a/VPNProxyExtension/Info.plist b/VPNProxyExtension/Info.plist new file mode 100644 index 0000000000..7f2489c298 --- /dev/null +++ b/VPNProxyExtension/Info.plist @@ -0,0 +1,17 @@ + + + + + DISTRIBUTED_NOTIFICATIONS_PREFIX + $(DISTRIBUTED_NOTIFICATIONS_PREFIX) + NETP_APP_GROUP + $(NETP_APP_GROUP) + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.app-proxy + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).MacTransparentProxyProvider + + + diff --git a/VPNProxyExtension/VPNProxyExtension.entitlements b/VPNProxyExtension/VPNProxyExtension.entitlements new file mode 100644 index 0000000000..968c758f97 --- /dev/null +++ b/VPNProxyExtension/VPNProxyExtension.entitlements @@ -0,0 +1,25 @@ + + + + + com.apple.security.application-groups + + $(TeamIdentifierPrefix)com.duckduckgo.macos.browser.network-protection + $(NETP_APP_GROUP) + + com.apple.security.app-sandbox + + com.apple.security.network.server + + keychain-access-groups + + $(NETP_APP_GROUP) + + com.apple.developer.networking.networkextension + + app-proxy-provider + + com.apple.security.network.client + + + diff --git a/fastlane/Matchfile b/fastlane/Matchfile index 8dbddfccb8..2af9eed1fe 100644 --- a/fastlane/Matchfile +++ b/fastlane/Matchfile @@ -11,6 +11,8 @@ app_identifier [ "com.duckduckgo.mobile.ios.review", "com.duckduckgo.mobile.ios.vpn.agent.review", "com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension", + "com.duckduckgo.mobile.ios.vpn.agent.proxy", + "com.duckduckgo.mobile.ios.vpn.agent.review.proxy", "com.duckduckgo.mobile.ios.DBP.backgroundAgent.review", "com.duckduckgo.mobile.ios.DBP.backgroundAgent" diff --git a/scripts/assets/AppStoreExportOptions.plist b/scripts/assets/AppStoreExportOptions.plist index b9395f914a..9ebf3d7885 100644 --- a/scripts/assets/AppStoreExportOptions.plist +++ b/scripts/assets/AppStoreExportOptions.plist @@ -14,12 +14,16 @@ match AppStore com.duckduckgo.mobile.ios.vpn.agent macos com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension match AppStore com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension macos + com.duckduckgo.mobile.ios.vpn.agent.proxy + match AppStore com.duckduckgo.mobile.ios.vpn.agent.proxy macos com.duckduckgo.mobile.ios.review match AppStore com.duckduckgo.mobile.ios.review macos com.duckduckgo.mobile.ios.vpn.agent.review match AppStore com.duckduckgo.mobile.ios.vpn.agent.review macos com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension match AppStore com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension macos + com.duckduckgo.mobile.ios.vpn.agent.review.proxy + match AppStore com.duckduckgo.mobile.ios.vpn.agent.review.proxy macos com.duckduckgo.mobile.ios.DBP.backgroundAgent match AppStore com.duckduckgo.mobile.ios.DBP.backgroundAgent macos com.duckduckgo.mobile.ios.DBP.backgroundAgent.review