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

Commit

Permalink
Exclude child binaries (#3824)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
diegoreymendez authored Feb 7, 2025
1 parent ab05170 commit 347b682
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 6 deletions.
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

0 comments on commit 347b682

Please sign in to comment.