Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit 347b682

Browse files
Exclude child binaries (#3824)
Task/Issue URL: https://app.asana.com/0/1206580121312550/1209309062840626/f ## Description See [Figma](https://www.figma.com/design/y7g8d3Nuefhfedq4638Rhu/VPN%3A-Domain-and-App-exclusions-on-Windows?node-id=134-18153&p=f&m=dev) for reference (keep in mind this is a Windows Figma, and there's no macOS one). Changes: - When a routing-rule is applied to an app through the VPN, its embedded binaries will be subjected to the same rules.
1 parent ab05170 commit 347b682

File tree

6 files changed

+199
-6
lines changed

6 files changed

+199
-6
lines changed

DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift

+10
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ final class VPNURLEventHandler {
9090
PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression)
9191
}
9292

93+
func showVPNAppExclusions() {
94+
windowControllerManager.showPreferencesTab(withSelectedPane: .vpn)
95+
windowControllerManager.showVPNAppExclusions()
96+
}
97+
98+
func showVPNDomainExclusions() {
99+
windowControllerManager.showPreferencesTab(withSelectedPane: .vpn)
100+
windowControllerManager.showVPNDomainExclusions()
101+
}
102+
93103
#if !APPSTORE && !DEBUG
94104
func moveAppToApplicationsFolder() {
95105
// this should be run after NSApplication.shared is set

DuckDuckGo/NetworkProtection/ExcludedApps/ExcludedAppsModel.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ protocol ExcludedAppsModel {
3131
}
3232

3333
final class DefaultExcludedAppsModel {
34-
private let appInfoRetriever: AppInfoRetrieveing = AppInfoRetriever()
34+
private let appInfoRetriever: AppInfoRetrieving = AppInfoRetriever()
3535
let proxySettings = TransparentProxySettings(defaults: .netP)
3636
private let pixelKit: PixelFiring?
3737

LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift

+108-4
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,71 @@
1919
import AppKit
2020
import Foundation
2121

22-
public protocol AppInfoRetrieveing {
22+
/// Protocol to provide a mechanism to query information about installed Applications.
23+
///
24+
public protocol AppInfoRetrieving {
2325

24-
/// Provides a structure featuring commonly-used app info.
26+
/// Provides a structure featuring commonly-used app info given the Application's bundleID.
2527
///
26-
/// It's also possible to retrieve the individual information directly by calling other methods in this class.
28+
/// - Parameters:
29+
/// - bundleID: the bundleID of the target Application.
2730
///
2831
func getAppInfo(bundleID: String) -> AppInfo?
32+
33+
/// Provides a structure featuring commonly-used app info, given the Application's URL.
34+
///
35+
/// - Parameters:
36+
/// - appURL: the URL where the target Application is installed.
37+
///
2938
func getAppInfo(appURL: URL) -> AppInfo?
39+
40+
/// Obtains the icon for a specified application.
41+
///
42+
/// - Parameters:
43+
/// - bundleID: the bundleID of the target Application.
44+
///
3045
func getAppIcon(bundleID: String) -> NSImage?
46+
47+
/// Obtains the URL for a specified application.
48+
///
49+
/// - Parameters:
50+
/// - bundleID: the bundleID of the target Application.
51+
///
52+
func getAppURL(bundleID: String) -> URL?
53+
54+
/// Obtains the visible name for a specified application.
55+
///
56+
/// - Parameters:
57+
/// - bundleID: the bundleID of the target Application.
58+
///
3159
func getAppName(bundleID: String) -> String?
60+
61+
/// Obtains the bundleID for a specified application.
62+
///
63+
/// - Parameters:
64+
/// - appURL: the URL where the target Application is installed.
65+
///
3266
func getBundleID(appURL: URL) -> String?
3367

68+
/// Obtains the bundleIDs for all Applications embedded within a speciried application.
69+
///
70+
/// - Parameters:
71+
/// - bundleURL: the URL where the parent Application is installed.
72+
///
73+
func findEmbeddedBundleIDs(in bundleURL: URL) -> Set<String>
3474
}
3575

36-
public class AppInfoRetriever: AppInfoRetrieveing {
76+
/// Provides a mechanism to query information about installed Applications.
77+
///
78+
public class AppInfoRetriever: AppInfoRetrieving {
3779

3880
public init() {}
3981

82+
/// Provides a structure featuring commonly-used app info given the Application's bundleID.
83+
///
84+
/// - Parameters:
85+
/// - bundleID: the bundleID of the target Application.
86+
///
4087
public func getAppInfo(bundleID: String) -> AppInfo? {
4188
guard let appName = getAppName(bundleID: bundleID) else {
4289
return nil
@@ -46,6 +93,11 @@ public class AppInfoRetriever: AppInfoRetrieveing {
4693
return AppInfo(bundleID: bundleID, name: appName, icon: appIcon)
4794
}
4895

96+
/// Provides a structure featuring commonly-used app info, given the Application's URL.
97+
///
98+
/// - Parameters:
99+
/// - appURL: the URL where the target Application is installed.
100+
///
49101
public func getAppInfo(appURL: URL) -> AppInfo? {
50102
guard let bundleID = getBundleID(appURL: appURL) else {
51103
return nil
@@ -54,6 +106,11 @@ public class AppInfoRetriever: AppInfoRetrieveing {
54106
return getAppInfo(bundleID: bundleID)
55107
}
56108

109+
/// Obtains the icon for a specified application.
110+
///
111+
/// - Parameters:
112+
/// - bundleID: the bundleID of the target Application.
113+
///
57114
public func getAppIcon(bundleID: String) -> NSImage? {
58115
guard let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) else {
59116
return nil
@@ -72,6 +129,11 @@ public class AppInfoRetriever: AppInfoRetrieveing {
72129
return NSImage(contentsOf: iconURL)
73130
}
74131

132+
/// Obtains the visible name for a specified application.
133+
///
134+
/// - Parameters:
135+
/// - bundleID: the bundleID of the target Application.
136+
///
75137
public func getAppName(bundleID: String) -> String? {
76138
if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) {
77139
// Try reading from Info.plist
@@ -86,6 +148,20 @@ public class AppInfoRetriever: AppInfoRetrieveing {
86148
return nil
87149
}
88150

151+
/// Obtains the URL for a specified application.
152+
///
153+
/// - Parameters:
154+
/// - bundleID: the bundleID of the target Application.
155+
///
156+
public func getAppURL(bundleID: String) -> URL? {
157+
NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID)
158+
}
159+
160+
/// Obtains the bundleID for a specified application.
161+
///
162+
/// - Parameters:
163+
/// - appURL: the URL where the target Application is installed.
164+
///
89165
public func getBundleID(appURL: URL) -> String? {
90166
let infoPlistURL = appURL.appendingPathComponent("Contents/Info.plist")
91167
if let plist = NSDictionary(contentsOf: infoPlistURL),
@@ -94,4 +170,32 @@ public class AppInfoRetriever: AppInfoRetrieveing {
94170
}
95171
return nil
96172
}
173+
174+
// MARK: - Embedded Bundle IDs
175+
176+
/// Obtains the bundleIDs for all Applications embedded within a speciried application.
177+
///
178+
/// - Parameters:
179+
/// - bundleURL: the URL where the parent Application is installed.
180+
///
181+
public func findEmbeddedBundleIDs(in bundleURL: URL) -> Set<String> {
182+
var bundleIDs: [String] = []
183+
let fileManager = FileManager.default
184+
185+
guard let enumerator = fileManager.enumerator(at: bundleURL,
186+
includingPropertiesForKeys: nil,
187+
options: [.skipsHiddenFiles],
188+
errorHandler: nil) else {
189+
return []
190+
}
191+
192+
for case let fileURL as URL in enumerator where fileURL.pathExtension == "app" {
193+
let embeddedBundle = Bundle(url: fileURL)
194+
if let bundleID = embeddedBundle?.bundleIdentifier {
195+
bundleIDs.append(bundleID)
196+
}
197+
}
198+
199+
return Set(bundleIDs)
200+
}
97201
}

LocalPackages/NetworkProtectionMac/Package.swift

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ let package = Package(
3535
dependencies: [
3636
.package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"),
3737
.package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"),
38+
.package(path: "../AppInfoRetriever"),
3839
.package(path: "../AppLauncher"),
3940
.package(path: "../UDSHelper"),
4041
.package(path: "../XPCHelper"),
@@ -62,6 +63,7 @@ let package = Package(
6263
.target(
6364
name: "NetworkProtectionProxy",
6465
dependencies: [
66+
"AppInfoRetriever",
6567
.product(name: "NetworkProtection", package: "BrowserServicesKit"),
6668
.product(name: "PixelKit", package: "BrowserServicesKit"),
6769
],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// AppRoutingRulesManager.swift
3+
//
4+
// Copyright © 2025 DuckDuckGo. All rights reserved.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
import AppInfoRetriever
20+
import Foundation
21+
import Combine
22+
23+
/// Manages App routing rules.
24+
///
25+
/// This manager expands the routing rules stored in the Proxy settings to include the bundleIDs
26+
/// of all embedded binaries. This is useful because when blocking or excluding an app the user
27+
/// likely expects the rule to extend to all child processes.
28+
///
29+
final class AppRoutingRulesManager {
30+
31+
private let appInfoRetriever: AppInfoRetrieving
32+
private(set) var rules: VPNAppRoutingRules
33+
private var cancellables = Set<AnyCancellable>()
34+
35+
init(settings: TransparentProxySettings,
36+
appInfoRetriever: AppInfoRetrieving = AppInfoRetriever()) {
37+
38+
self.appInfoRetriever = appInfoRetriever
39+
self.rules = Self.expandAppRoutingRules(settings.appRoutingRules, appInfoRetriever: appInfoRetriever)
40+
41+
subscribeToAppRoutingRulesChanges(settings)
42+
}
43+
44+
static func expandAppRoutingRules(_ rules: VPNAppRoutingRules,
45+
appInfoRetriever: AppInfoRetrieving) -> VPNAppRoutingRules {
46+
47+
var expandedRules = rules
48+
49+
for (bundleID, rule) in rules {
50+
guard let bundleURL = appInfoRetriever.getAppURL(bundleID: bundleID) else {
51+
continue
52+
}
53+
54+
let embeddedAppBundleIDs = appInfoRetriever.findEmbeddedBundleIDs(in: bundleURL)
55+
56+
for childBundleID in embeddedAppBundleIDs {
57+
expandedRules[childBundleID] = rule
58+
}
59+
}
60+
61+
return expandedRules
62+
}
63+
64+
private func subscribeToAppRoutingRulesChanges(_ settings: TransparentProxySettings) {
65+
settings.appRoutingRulesPublisher
66+
.receive(on: DispatchQueue.main)
67+
.map { [appInfoRetriever] rules in
68+
return Self.expandAppRoutingRules(rules, appInfoRetriever: appInfoRetriever)
69+
}
70+
.assign(to: \.rules, onWeaklyHeld: self)
71+
.store(in: &cancellables)
72+
}
73+
}

LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Provider/TransparentProxyProvider.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// limitations under the License.
1717
//
1818

19+
import AppInfoRetriever
1920
import Combine
2021
import Foundation
2122
import NetworkExtension
@@ -91,6 +92,7 @@ open class TransparentProxyProvider: NETransparentProxyProvider {
9192
@MainActor
9293
public var isRunning = false
9394

95+
private let appRoutingRulesManager: AppRoutingRulesManager
9496
private let logger: Logger
9597
private let appMessageHandler: TransparentProxyAppMessageHandler
9698
private let eventHandler: TransparentProxyProviderEventHandler
@@ -108,6 +110,8 @@ open class TransparentProxyProvider: NETransparentProxyProvider {
108110
self.settings = settings
109111
self.eventHandler = eventHandler
110112

113+
appRoutingRulesManager = AppRoutingRulesManager(settings: settings)
114+
111115
super.init()
112116

113117
subscribeToSettings()
@@ -445,7 +449,7 @@ open class TransparentProxyProvider: NETransparentProxyProvider {
445449
private func path(for flow: NEAppProxyFlow) -> FlowPath {
446450
let appIdentifier = flow.metaData.sourceAppSigningIdentifier
447451

448-
switch settings.appRoutingRules[appIdentifier] {
452+
switch appRoutingRulesManager.rules[appIdentifier] {
449453
case .none:
450454
if let hostname = flow.remoteHostname,
451455
isExcludedDomain(hostname) {

0 commit comments

Comments
 (0)