Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude child binaries #3824

Merged
merged 18 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ final class VPNURLEventHandler {
PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression)
}

func showVPNAppExclusions() {
windowControllerManager.showPreferencesTab(withSelectedPane: .vpn)
windowControllerManager.showVPNAppExclusions()
}

func showVPNDomainExclusions() {
windowControllerManager.showPreferencesTab(withSelectedPane: .vpn)
windowControllerManager.showVPNDomainExclusions()
}

#if !APPSTORE && !DEBUG
func moveAppToApplicationsFolder() {
// this should be run after NSApplication.shared is set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ protocol ExcludedAppsModel {
}

final class DefaultExcludedAppsModel {
private let appInfoRetriever: AppInfoRetrieveing = AppInfoRetriever()
private let appInfoRetriever: AppInfoRetrieving = AppInfoRetriever()
let proxySettings = TransparentProxySettings(defaults: .netP)
private let pixelKit: PixelFiring?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,71 @@
import AppKit
import Foundation

public protocol AppInfoRetrieveing {
/// Protocol to provide a mechanism to query information about installed Applications.
///
public protocol AppInfoRetrieving {

/// Provides a structure featuring commonly-used app info.
/// Provides a structure featuring commonly-used app info given the Application's bundleID.
///
/// It's also possible to retrieve the individual information directly by calling other methods in this class.
/// - Parameters:
/// - bundleID: the bundleID of the target Application.
///
func getAppInfo(bundleID: String) -> AppInfo?

/// Provides a structure featuring commonly-used app info, given the Application's URL.
///
/// - Parameters:
/// - appURL: the URL where the target Application is installed.
///
func getAppInfo(appURL: URL) -> AppInfo?

/// Obtains the icon for a specified application.
///
/// - Parameters:
/// - bundleID: the bundleID of the target Application.
///
func getAppIcon(bundleID: String) -> NSImage?

/// Obtains the URL for a specified application.
///
/// - Parameters:
/// - bundleID: the bundleID of the target Application.
///
func getAppURL(bundleID: String) -> URL?

/// Obtains the visible name for a specified application.
///
/// - Parameters:
/// - bundleID: the bundleID of the target Application.
///
func getAppName(bundleID: String) -> String?

/// Obtains the bundleID for a specified application.
///
/// - Parameters:
/// - appURL: the URL where the target Application is installed.
///
func getBundleID(appURL: URL) -> String?

/// Obtains the bundleIDs for all Applications embedded within a speciried application.
///
/// - Parameters:
/// - bundleURL: the URL where the parent Application is installed.
///
func findEmbeddedBundleIDs(in bundleURL: URL) -> Set<String>
}

public class AppInfoRetriever: AppInfoRetrieveing {
/// Provides a mechanism to query information about installed Applications.
///
public class AppInfoRetriever: AppInfoRetrieving {

public init() {}

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

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

/// Obtains the icon for a specified application.
///
/// - Parameters:
/// - bundleID: the bundleID of the target Application.
///
public func getAppIcon(bundleID: String) -> NSImage? {
guard let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) else {
return nil
Expand All @@ -72,6 +129,11 @@ public class AppInfoRetriever: AppInfoRetrieveing {
return NSImage(contentsOf: iconURL)
}

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

/// Obtains the URL for a specified application.
///
/// - Parameters:
/// - bundleID: the bundleID of the target Application.
///
public func getAppURL(bundleID: String) -> URL? {
NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID)
}

/// Obtains the bundleID for a specified application.
///
/// - Parameters:
/// - appURL: the URL where the target Application is installed.
///
public func getBundleID(appURL: URL) -> String? {
let infoPlistURL = appURL.appendingPathComponent("Contents/Info.plist")
if let plist = NSDictionary(contentsOf: infoPlistURL),
Expand All @@ -94,4 +170,32 @@ public class AppInfoRetriever: AppInfoRetrieveing {
}
return nil
}

// MARK: - Embedded Bundle IDs

/// Obtains the bundleIDs for all Applications embedded within a speciried application.
///
/// - Parameters:
/// - bundleURL: the URL where the parent Application is installed.
///
public func findEmbeddedBundleIDs(in bundleURL: URL) -> Set<String> {
var bundleIDs: [String] = []
let fileManager = FileManager.default

guard let enumerator = fileManager.enumerator(at: bundleURL,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles],
errorHandler: nil) else {
return []
}

for case let fileURL as URL in enumerator where fileURL.pathExtension == "app" {
let embeddedBundle = Bundle(url: fileURL)
if let bundleID = embeddedBundle?.bundleIdentifier {
bundleIDs.append(bundleID)
}
}

return Set(bundleIDs)
}
}
2 changes: 2 additions & 0 deletions LocalPackages/NetworkProtectionMac/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"),
.package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"),
.package(path: "../AppInfoRetriever"),
.package(path: "../AppLauncher"),
.package(path: "../UDSHelper"),
.package(path: "../XPCHelper"),
Expand Down Expand Up @@ -62,6 +63,7 @@ let package = Package(
.target(
name: "NetworkProtectionProxy",
dependencies: [
"AppInfoRetriever",
.product(name: "NetworkProtection", package: "BrowserServicesKit"),
.product(name: "PixelKit", package: "BrowserServicesKit"),
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// AppRoutingRulesManager.swift
//
// Copyright © 2025 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 AppInfoRetriever
import Foundation
import Combine

/// Manages App routing rules.
///
/// This manager expands the routing rules stored in the Proxy settings to include the bundleIDs
/// of all embedded binaries. This is useful because when blocking or excluding an app the user
/// likely expects the rule to extend to all child processes.
///
final class AppRoutingRulesManager {

private let appInfoRetriever: AppInfoRetrieving
private(set) var rules: VPNAppRoutingRules
private var cancellables = Set<AnyCancellable>()

init(settings: TransparentProxySettings,
appInfoRetriever: AppInfoRetrieving = AppInfoRetriever()) {

self.appInfoRetriever = appInfoRetriever
self.rules = Self.expandAppRoutingRules(settings.appRoutingRules, appInfoRetriever: appInfoRetriever)

subscribeToAppRoutingRulesChanges(settings)
}

static func expandAppRoutingRules(_ rules: VPNAppRoutingRules,
appInfoRetriever: AppInfoRetrieving) -> VPNAppRoutingRules {

var expandedRules = rules

for (bundleID, rule) in rules {
guard let bundleURL = appInfoRetriever.getAppURL(bundleID: bundleID) else {
continue
}

let embeddedAppBundleIDs = appInfoRetriever.findEmbeddedBundleIDs(in: bundleURL)

for childBundleID in embeddedAppBundleIDs {
expandedRules[childBundleID] = rule
}
}

return expandedRules
}

private func subscribeToAppRoutingRulesChanges(_ settings: TransparentProxySettings) {
settings.appRoutingRulesPublisher
.receive(on: DispatchQueue.main)
.map { [appInfoRetriever] rules in
return Self.expandAppRoutingRules(rules, appInfoRetriever: appInfoRetriever)
}
.assign(to: \.rules, onWeaklyHeld: self)
.store(in: &cancellables)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// limitations under the License.
//

import AppInfoRetriever
import Combine
import Foundation
import NetworkExtension
Expand Down Expand Up @@ -91,6 +92,7 @@ open class TransparentProxyProvider: NETransparentProxyProvider {
@MainActor
public var isRunning = false

private let appRoutingRulesManager: AppRoutingRulesManager
private let logger: Logger
private let appMessageHandler: TransparentProxyAppMessageHandler
private let eventHandler: TransparentProxyProviderEventHandler
Expand All @@ -108,6 +110,8 @@ open class TransparentProxyProvider: NETransparentProxyProvider {
self.settings = settings
self.eventHandler = eventHandler

appRoutingRulesManager = AppRoutingRulesManager(settings: settings)

super.init()

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

switch settings.appRoutingRules[appIdentifier] {
switch appRoutingRulesManager.rules[appIdentifier] {
case .none:
if let hostname = flow.remoteHostname,
isExcludedDomain(hostname) {
Expand Down
Loading