From b8809c6552df559b06e72db89ce340d5574bbfce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:29:47 +0000 Subject: [PATCH 1/4] Initial plan From 473283afdd26a7962bac831b64bd30f74f4f10ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:34:37 +0000 Subject: [PATCH 2/4] Add process-based exclusion support using executable path extraction Co-authored-by: Anof-cyber <39705906+Anof-cyber@users.noreply.github.com> --- .../extension/AppProxyProvider.swift | 98 +++++++++++++++---- MacOS/README.md | 54 +++++++++- 2 files changed, 130 insertions(+), 22 deletions(-) diff --git a/MacOS/ProxyBridge/extension/AppProxyProvider.swift b/MacOS/ProxyBridge/extension/AppProxyProvider.swift index ac6cd18..a65a7e0 100644 --- a/MacOS/ProxyBridge/extension/AppProxyProvider.swift +++ b/MacOS/ProxyBridge/extension/AppProxyProvider.swift @@ -217,6 +217,47 @@ class AppProxyProvider: NETransparentProxyProvider { } logQueueLock.unlock() } + + // Helper function to extract process executable path from audit token + private func getProcessPath(from auditToken: Data) -> String? { + guard auditToken.count == MemoryLayout.size else { + return nil + } + + var token = audit_token_t() + _ = withUnsafeMutableBytes(of: &token) { tokenPtr in + auditToken.copyBytes(to: tokenPtr) + } + + let pid = audit_token_to_pid(token) + + let pathBufferSize = Int(PROC_PIDPATHINFO_MAXSIZE) + var pathBuffer = [CChar](repeating: 0, count: pathBufferSize) + + let result = pathBuffer.withUnsafeMutableBufferPointer { bufferPtr in + proc_pidpath(pid, bufferPtr.baseAddress, UInt32(pathBufferSize)) + } + + guard result > 0 else { + return nil + } + + return String(cString: pathBuffer) + } + + // Helper function to extract process identifier from NEFlowMetaData + // Returns both the bundle ID (if available) and the executable path + private func getProcessIdentifiers(from metaData: NEFlowMetaData) -> (bundleId: String, executablePath: String?) { + let bundleId = metaData.sourceAppSigningIdentifier + var executablePath: String? = nil + + // Try to get executable path from audit token if available + if let auditToken = metaData.sourceAppAuditToken { + executablePath = getProcessPath(from: auditToken) + } + + return (bundleId, executablePath) + } override func startProxy(options: [String : Any]?, completionHandler: @escaping (Error?) -> Void) { let settings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1") @@ -447,12 +488,19 @@ class AppProxyProvider: NETransparentProxyProvider { portStr = "unknown" } - var processPath = "unknown" + var processIdentifier = "unknown" + var executablePath: String? = nil + if let metaData = flow.metaData as? NEFlowMetaData { - processPath = metaData.sourceAppSigningIdentifier + let identifiers = getProcessIdentifiers(from: metaData) + processIdentifier = identifiers.bundleId + executablePath = identifiers.executablePath } - if processPath == "com.interceptsuite.ProxyBridge" || processPath == "com.interceptsuite.ProxyBridge.extension" { + // Use executable path for logging/display if available, otherwise use bundle ID + let displayIdentifier = executablePath ?? processIdentifier + + if processIdentifier == "com.interceptsuite.ProxyBridge" || processIdentifier == "com.interceptsuite.ProxyBridge.extension" { return false } @@ -461,16 +509,16 @@ class AppProxyProvider: NETransparentProxyProvider { proxyLock.unlock() if !hasProxyConfig { - sendLogToApp(protocol: "TCP", process: processPath, destination: destination, port: portStr, proxy: "Direct") + sendLogToApp(protocol: "TCP", process: displayIdentifier, destination: destination, port: portStr, proxy: "Direct") return false } - let matchedRule = findMatchingRule(processPath: processPath, destination: destination, port: portNum, connectionProtocol: .tcp, checkIpPort: true) + let matchedRule = findMatchingRule(bundleId: processIdentifier, executablePath: executablePath, destination: destination, port: portNum, connectionProtocol: .tcp, checkIpPort: true) if let rule = matchedRule { let action = rule.action.rawValue - sendLogToApp(protocol: "TCP", process: processPath, destination: destination, port: portStr, proxy: action) + sendLogToApp(protocol: "TCP", process: displayIdentifier, destination: destination, port: portStr, proxy: action) switch rule.action { case .direct: @@ -484,18 +532,25 @@ class AppProxyProvider: NETransparentProxyProvider { return true } } else { - sendLogToApp(protocol: "TCP", process: processPath, destination: destination, port: portStr, proxy: "Direct") + sendLogToApp(protocol: "TCP", process: displayIdentifier, destination: destination, port: portStr, proxy: "Direct") return false } } private func handleUDPFlow(_ flow: NEAppProxyUDPFlow) -> Bool { - var processPath = "unknown" + var processIdentifier = "unknown" + var executablePath: String? = nil + if let metaData = flow.metaData as? NEFlowMetaData { - processPath = metaData.sourceAppSigningIdentifier + let identifiers = getProcessIdentifiers(from: metaData) + processIdentifier = identifiers.bundleId + executablePath = identifiers.executablePath } - if processPath == "com.interceptsuite.ProxyBridge" || processPath == "com.interceptsuite.ProxyBridge.extension" { + // Use executable path for logging/display if available, otherwise use bundle ID + let displayIdentifier = executablePath ?? processIdentifier + + if processIdentifier == "com.interceptsuite.ProxyBridge" || processIdentifier == "com.interceptsuite.ProxyBridge.extension" { return false } @@ -509,16 +564,16 @@ class AppProxyProvider: NETransparentProxyProvider { return false } - let matchedRule = findMatchingRule(processPath: processPath, destination: "", port: 0, connectionProtocol: .udp, checkIpPort: false) + let matchedRule = findMatchingRule(bundleId: processIdentifier, executablePath: executablePath, destination: "", port: 0, connectionProtocol: .udp, checkIpPort: false) if let rule = matchedRule { // We don't have access to UDP dest ip and port when os handles it in (apple proxy API limitation), we log with unknown ip and port to know specific package is using UDP switch rule.action { case .direct: - sendLogToApp(protocol: "UDP", process: processPath, destination: "unknown", port: "unknown", proxy: "Direct") + sendLogToApp(protocol: "UDP", process: displayIdentifier, destination: "unknown", port: "unknown", proxy: "Direct") return false case .block: - sendLogToApp(protocol: "UDP", process: processPath, destination: "unknown", port: "unknown", proxy: "BLOCK") + sendLogToApp(protocol: "UDP", process: displayIdentifier, destination: "unknown", port: "unknown", proxy: "BLOCK") return true case .proxy: flow.open(withLocalEndpoint: nil) { [weak self] error in @@ -530,14 +585,14 @@ class AppProxyProvider: NETransparentProxyProvider { } if let host = socksHost, let port = socksPort { - self.proxyUDPFlowViaSOCKS5(flow, processPath: processPath, socksHost: host, socksPort: port) + self.proxyUDPFlowViaSOCKS5(flow, processPath: displayIdentifier, socksHost: host, socksPort: port) } } return true } } else { // No rule matched let OS handle it, but log it so user knows this process is using UDP - sendLogToApp(protocol: "UDP", process: processPath, destination: "unknown", port: "unknown", proxy: "Direct") + sendLogToApp(protocol: "UDP", process: displayIdentifier, destination: "unknown", port: "unknown", proxy: "Direct") return false } } @@ -1150,7 +1205,7 @@ class AppProxyProvider: NETransparentProxyProvider { override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { } - private func findMatchingRule(processPath: String, destination: String, port: UInt16, connectionProtocol: RuleProtocol, checkIpPort: Bool) -> ProxyRule? { + private func findMatchingRule(bundleId: String, executablePath: String?, destination: String, port: UInt16, connectionProtocol: RuleProtocol, checkIpPort: Bool) -> ProxyRule? { rulesLock.lock() defer { rulesLock.unlock() } @@ -1161,7 +1216,16 @@ class AppProxyProvider: NETransparentProxyProvider { continue } - if !rule.matchesProcess(processPath) { + // Try matching against executable path first (if available), then bundle ID + var processMatched = false + if let execPath = executablePath { + processMatched = rule.matchesProcess(execPath) + } + if !processMatched { + processMatched = rule.matchesProcess(bundleId) + } + + if !processMatched { continue } diff --git a/MacOS/README.md b/MacOS/README.md index f1fe828..290ca76 100644 --- a/MacOS/README.md +++ b/MacOS/README.md @@ -236,7 +236,7 @@ Rules determine how network traffic is handled. Multiple rules can be created, a | Component | Description | Supports TCP | Supports UDP | |-----------|-------------|--------------|--------------| -| Package Name | Application bundle identifier | Yes | Yes | +| Package Name | Application bundle identifier or executable path/name | Yes | Yes | | IP/Hostname | Destination IP address or domain | Yes | No* | | Port | Destination port number | Yes | No* | | Protocol | TCP, UDP, or Both | Yes | Yes | @@ -244,6 +244,21 @@ Rules determine how network traffic is handled. Multiple rules can be created, a **Note:** UDP rules only match on package name due to Apple API limitations. IP and port-based filtering is not available for UDP traffic. +##### Process Identification + +ProxyBridge can identify processes in two ways: + +1. **Bundle Identifier** (e.g., `com.google.Chrome`) - Available for all applications with app bundles +2. **Executable Path/Name** - Available for standalone binaries and processes without bundle identifiers + +When creating rules, you can use either: +- **Full executable paths**: `/usr/local/bin/ciadpi` or `/Applications/MyApp.app/Contents/MacOS/MyApp` +- **Executable names**: `ciadpi` or `MyApp` (matches any process with this name) +- **Bundle identifiers**: `com.example.app` (traditional bundle ID matching) +- **Wildcards**: `ciadpi*`, `*proxy`, or `com.example.*` + +**Matching Priority**: When both bundle ID and executable path are available, ProxyBridge tries to match the rule against the executable path first, then falls back to bundle ID matching. This ensures maximum compatibility with both bundled applications and standalone binaries. + #### Actions - **PROXY** - Route traffic through the configured proxy server @@ -301,6 +316,33 @@ Protocol: UDP Action: PROXY ``` +**Exclude standalone binary from proxy (e.g., DPI bypass tools)** +``` +Package Name: ciadpi +IP/Hostname: (empty) +Port: (empty) +Protocol: Both +Action: DIRECT +``` + +**Exclude by full executable path** +``` +Package Name: /usr/local/bin/ciadpi +IP/Hostname: (empty) +Port: (empty) +Protocol: Both +Action: DIRECT +``` + +**Proxy all binaries starting with "proxy"** +``` +Package Name: proxy* +IP/Hostname: (empty) +Port: (empty) +Protocol: TCP +Action: PROXY +``` + #### Exporting and Importing Rules ProxyBridge allows you to export selected rules to a JSON file and import rules from previously exported files. @@ -360,10 +402,12 @@ Connection logs are available in the main window, showing: ### Apple Network Extension API Constraints -1. **Package Name vs Process Name** - - Rules use application bundle identifier (package name), not process name - - Apple's Network Extension API does not provide access to process names - - Use `com.example.app` format instead of executable names +1. **Process Identification** + - **Primary Method**: Rules primarily use application bundle identifier (e.g., `com.example.app`) + - **Extended Support**: ProxyBridge now supports matching by executable path/name for standalone binaries + - **How it works**: The extension extracts the executable path using the process audit token when available + - **Fallback**: If executable path cannot be determined, bundle identifier matching is still used + - **Note**: This is a workaround to Apple's Network Extension API limitations and may not work in all edge cases 2. **UDP Traffic Limitations** - UDP rules can only match on package name From ae68128b67314864bc25852222633565534d4dc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:35:49 +0000 Subject: [PATCH 3/4] Add documentation and explicit Darwin import for process path functions Co-authored-by: Anof-cyber <39705906+Anof-cyber@users.noreply.github.com> --- .../extension/AppProxyProvider.swift | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/MacOS/ProxyBridge/extension/AppProxyProvider.swift b/MacOS/ProxyBridge/extension/AppProxyProvider.swift index a65a7e0..054dd6a 100644 --- a/MacOS/ProxyBridge/extension/AppProxyProvider.swift +++ b/MacOS/ProxyBridge/extension/AppProxyProvider.swift @@ -1,4 +1,5 @@ import NetworkExtension +import Darwin // For audit_token_t, audit_token_to_pid, proc_pidpath, PROC_PIDPATHINFO_MAXSIZE enum RuleProtocol: String, Codable { case tcp = "TCP" @@ -218,7 +219,21 @@ class AppProxyProvider: NETransparentProxyProvider { logQueueLock.unlock() } - // Helper function to extract process executable path from audit token + /// Extracts the executable path of a process from its audit token. + /// + /// This function uses the audit token to get the process ID (PID), then retrieves + /// the full path to the process executable using proc_pidpath(). + /// + /// - Parameter auditToken: The audit token data from NEFlowMetaData.sourceAppAuditToken + /// - Returns: The full path to the process executable, or nil if: + /// - The audit token size is invalid + /// - The process no longer exists + /// - The calling process lacks permission to access the process information + /// - proc_pidpath() fails for any other reason + /// + /// - Note: This is a workaround for Apple's Network Extension API limitation that doesn't + /// provide direct access to process names or paths. It may fail in some edge cases + /// due to security restrictions or race conditions. private func getProcessPath(from auditToken: Data) -> String? { guard auditToken.count == MemoryLayout.size else { return nil @@ -231,7 +246,7 @@ class AppProxyProvider: NETransparentProxyProvider { let pid = audit_token_to_pid(token) - let pathBufferSize = Int(PROC_PIDPATHINFO_MAXSIZE) + let pathBufferSize = Int(PROC_PIDPATHINFO_MAXSIZE) // From Darwin module var pathBuffer = [CChar](repeating: 0, count: pathBufferSize) let result = pathBuffer.withUnsafeMutableBufferPointer { bufferPtr in @@ -245,8 +260,20 @@ class AppProxyProvider: NETransparentProxyProvider { return String(cString: pathBuffer) } - // Helper function to extract process identifier from NEFlowMetaData - // Returns both the bundle ID (if available) and the executable path + /// Extracts process identifiers from network flow metadata. + /// + /// This function retrieves both the bundle identifier (always available) and the + /// executable path (when available) for the process that originated the network flow. + /// + /// - Parameter metaData: The NEFlowMetaData from the network flow + /// - Returns: A tuple containing: + /// - bundleId: The application bundle identifier (e.g., "com.example.app") or "unknown" + /// - executablePath: The full path to the executable (e.g., "/usr/local/bin/ciadpi"), + /// or nil if the path cannot be determined + /// + /// - Note: The executable path is extracted from the sourceAppAuditToken when available. + /// For processes without app bundles (standalone binaries), this provides the + /// only reliable way to identify the process. private func getProcessIdentifiers(from metaData: NEFlowMetaData) -> (bundleId: String, executablePath: String?) { let bundleId = metaData.sourceAppSigningIdentifier var executablePath: String? = nil From 121b96e0e14e55728991cebb7f0ba425e7dc80f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:37:57 +0000 Subject: [PATCH 4/4] Refactor code based on review feedback: rename function, remove redundant comment, simplify logic Co-authored-by: Anof-cyber <39705906+Anof-cyber@users.noreply.github.com> --- MacOS/ProxyBridge/extension/AppProxyProvider.swift | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/MacOS/ProxyBridge/extension/AppProxyProvider.swift b/MacOS/ProxyBridge/extension/AppProxyProvider.swift index 054dd6a..6ced37d 100644 --- a/MacOS/ProxyBridge/extension/AppProxyProvider.swift +++ b/MacOS/ProxyBridge/extension/AppProxyProvider.swift @@ -234,7 +234,7 @@ class AppProxyProvider: NETransparentProxyProvider { /// - Note: This is a workaround for Apple's Network Extension API limitation that doesn't /// provide direct access to process names or paths. It may fail in some edge cases /// due to security restrictions or race conditions. - private func getProcessPath(from auditToken: Data) -> String? { + private func getExecutablePath(from auditToken: Data) -> String? { guard auditToken.count == MemoryLayout.size else { return nil } @@ -246,7 +246,7 @@ class AppProxyProvider: NETransparentProxyProvider { let pid = audit_token_to_pid(token) - let pathBufferSize = Int(PROC_PIDPATHINFO_MAXSIZE) // From Darwin module + let pathBufferSize = Int(PROC_PIDPATHINFO_MAXSIZE) var pathBuffer = [CChar](repeating: 0, count: pathBufferSize) let result = pathBuffer.withUnsafeMutableBufferPointer { bufferPtr in @@ -280,7 +280,7 @@ class AppProxyProvider: NETransparentProxyProvider { // Try to get executable path from audit token if available if let auditToken = metaData.sourceAppAuditToken { - executablePath = getProcessPath(from: auditToken) + executablePath = getExecutablePath(from: auditToken) } return (bundleId, executablePath) @@ -1244,13 +1244,7 @@ class AppProxyProvider: NETransparentProxyProvider { } // Try matching against executable path first (if available), then bundle ID - var processMatched = false - if let execPath = executablePath { - processMatched = rule.matchesProcess(execPath) - } - if !processMatched { - processMatched = rule.matchesProcess(bundleId) - } + let processMatched = executablePath.map { rule.matchesProcess($0) } ?? false || rule.matchesProcess(bundleId) if !processMatched { continue