Post

Replies

Boosts

Views

Activity

Reply to Implement web filter in macOS with NetworkExtension API
Hi @Systems Engineer @rmalviya @yblk , Matt DESCRIPTION OF PROBLEM :- We are currently developing a macOS app using the NEFilterDataProvider in the Network Extension framework, and we've encountered an issue regarding hostname resolution that we would like your guidance on. In our implementation, we need to drop network flows based on the hostname. The app successfully receives the remoteHostname or remoteEndpoint.hostname for browsers such as Safari and Mozilla Firefox. However, for other browsers like Chrome, Opera Mini, Arc, Brave, and Edge, we only receive the IP address instead of the hostname. We are particularly looking for a way to retrieve the hostname for all browsers to apply our filtering logic consistently. Could you please advise whether there is any additional configuration or API we can use to ensure that we receive hostnames for these browsers as well? Alternatively, is this a limitation of the browsers themselves, and should we expect to only receive IP addresses for certain cases? STEPS TO REPRODUCE :- For Chrome, Brave, Edge, and Arc browsers you won't receive the hostname in NEFilterFlow. Using the same sample project provided in WWDC 2019 https://developer.apple.com/documentation/networkextension/filtering_network_traffic SUPPORT INFORMATION :- Did someone from Apple ask you to submit a code-level support request? No Do you have a focused test project that demonstrates your issue? Yes, I have a focused test project to submit with my request What code level support issue are you having? Problems with an Apple framework API in my app
Topic: App & System Services SubTopic: Core OS Tags:
Oct ’24
Reply to How to avoid my local server flows in Transparent App Proxy
Thank you for your prompt response and the clarification. I’ve reviewed your code, which appears to be quite effective in identifying the source application. However, I’m encountering an issue where retrieving the bundle ID for certain flows results in an error code 100001. Interestingly, the code works as expected for system applications. Could you please advise on the correct approach to reliably obtain the PID and bundle ID for any app flow? I’d appreciate it if you could let me know if I’ve misunderstood or missed something in the implementation. func bundleIDForAuditToken(_ tokenData: Data) -> String? { // Get a code reference. var codeQ: SecCode? = nil var err = SecCodeCopyGuestWithAttributes(nil, [ kSecGuestAttributeAudit: tokenData ] as NSDictionary, [], &codeQ) guard err == errSecSuccess else { return nil } let code = codeQ! // Convert that to a static code. var staticCodeQ: SecStaticCode? = nil err = SecCodeCopyStaticCode(code, [], &staticCodeQ) guard err == errSecSuccess else { return nil } let staticCode = staticCodeQ! // Get code signing information about that. var infoQ: CFDictionary? = nil err = SecCodeCopySigningInformation(staticCode, [], &infoQ) guard err == errSecSuccess else { return nil } let info = infoQ! as! [String:Any] // Extract the bundle ID from that. guard let plist = info[kSecCodeInfoPList as String] as? [String:Any], let bundleID = plist[kCFBundleIdentifierKey as String] as? String else { return nil } return bundleID }
Apr ’25
Reply to How to avoid my local server flows in Transparent App Proxy
Yes, you were right. The App sandbox was blocking it. Now I am able to extract bundleId, but I don't understand why I can't connect with the host and load the website. It would help if you reviewed the code and guided me further if I were missing something. import Foundation import NetworkExtension import os.log class AppProxyProvider: NETransparentProxyProvider { private let log = OSLog(subsystem: "com.proxy.tcp.network.extension", category: "provider") override func startProxy(options: [String: Any]?, completionHandler: @escaping (Error?) -> Void) { os_log(.info, log: log, "Starting Transparent Proxy %{public}@", "") // Configure the network settings for the tunnel let proxySettings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1") // Include both TCP and UDP rules for complete coverage proxySettings.includedNetworkRules = [ NENetworkRule( remoteNetwork: nil, remotePrefix: 0, localNetwork: nil, localPrefix: 0, protocol: .TCP, direction: .outbound ), NENetworkRule( remoteNetwork: nil, remotePrefix: 0, localNetwork: nil, localPrefix: 0, protocol: .UDP, direction: .outbound ) ] // Apply the settings setTunnelNetworkSettings(proxySettings) { error in if let error = error { os_log(.error, log: self.log, "Failed to apply tunnel settings: %{public}@", error.localizedDescription) } else { os_log(.info, log: self.log, "Tunnel settings applied successfully %{public}@", "") } completionHandler(error) } } override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { os_log(.info, log: log, "Stopping proxy with reason: %{public}d", reason.rawValue) // Cancel any ongoing operations setTunnelNetworkSettings(nil) { error in if let error = error { os_log(.error, log: self.log, "Error clearing tunnel settings: %{public}@", error.localizedDescription) } completionHandler() } } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { guard let message = String(data: messageData, encoding: .utf8) else { os_log(.error, log: log, "Received invalid message data %{public}@", "") completionHandler?(nil) return } os_log(.debug, log: log, "Received message: %{public}@", message) // Handle specific message types if message == "get_mapping" { let responseMessage = "No active mappings" completionHandler?(responseMessage.data(using: .utf8)) } else { // Default response completionHandler?(messageData) } } override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool { os_log(.debug, log: log, "Handling new flow of type: %{public}@", String(describing: type(of: flow))) // Handle TCP flows if let tcpFlow = flow as? NEAppProxyTCPFlow { guard let remoteEndpoint = tcpFlow.remoteEndpoint as? NWHostEndpoint else { os_log(.error, log: log, "Unable to cast remote endpoint to NWHostEndpoint %{public}@", "") return false } os_log(.info, log: log, "New TCP flow to %{public}@:%{public}@", remoteEndpoint.hostname, remoteEndpoint.port) if let token = flow.metaData.sourceAppAuditToken { let bundleIdFromData = self.bundleIDForAuditToken(token) //My this app bundle ID if let bundleID = bundleIdFromData, bundleID.contains("com.promobi.TransparentProxyMacApril") { os_log(.error, log: log, "Skipping the flow as it is not from my app %{public}@", bundleID) return false } } // Create a TCP connection manager for this flow let connectionManager = TransparentProxyManager(flow: tcpFlow, endpoint: remoteEndpoint) connectionManager.startExchangingData() return true } // Handle UDP flows if needed else if let udpFlow = flow as? NEAppProxyUDPFlow { os_log(.info, log: log, "UDP flow received - not handling in this implementation %{public}@", "") return false } // Unsupported flow type os_log(.error, log: log, "Unsupported flow type: %{public}@", String(describing: type(of: flow))) return false } func bundleIDForAuditToken(_ tokenData: Data) -> String? { // Create logger let log = OSLog(subsystem: "com.yourcompany.bundleid", category: "AuditToken") // Get a code reference. var codeQ: SecCode? = nil var err = SecCodeCopyGuestWithAttributes(nil, [ kSecGuestAttributeAudit: tokenData ] as NSDictionary, [], &codeQ) guard err == errSecSuccess else { os_log("Failed to copy guest with attributes: %{public}d", log: log, type: .error, err) return nil } let code = codeQ! // Convert that to a static code. var staticCodeQ: SecStaticCode? = nil err = SecCodeCopyStaticCode(code, [], &staticCodeQ) guard err == errSecSuccess else { os_log("Failed to copy static code: %{public}d", log: log, type: .error, err) return nil } let staticCode = staticCodeQ! // Get code signing information about that. var infoQ: CFDictionary? = nil err = SecCodeCopySigningInformation(staticCode, [], &infoQ) guard err == errSecSuccess else { os_log("Failed to copy signing information: %{public}d", log: log, type: .error, err) return nil } let info = infoQ! as! [String:Any] // Extract the bundle ID from that. guard let plist = info[kSecCodeInfoPList as String] as? [String:Any], let bundleID = plist[kCFBundleIdentifierKey as String] as? String else { os_log("Failed to extract bundle ID from info dictionary", log: log, type: .error) return nil } os_log("Successfully extracted bundle ID: %{public}@", log: log, type: .debug, bundleID) return bundleID } }
Apr ’25
Reply to How to avoid my local server flows in Transparent App Proxy
private func stateChangedCallback(to state: NWConnection.State) { switch state { case .ready: os_log(.debug, log: self.log, "TransparentProxyManager::stateChangedCallback. TCP connection is ready %{public}@", "") // Call the correct open method flow.open(withLocalEndpoint: nil) { error in if let error = error { os_log(.error, log: self.log, "TCP flow opening failed %{public}@", error.localizedDescription) self.connection.cancel() return } os_log(.debug, log: self.log, "TCP flow opened successfully, starting data exchange %{public}@", "") self.handleOutgoingTCPData() } case .failed(let error): os_log(.error, log: self.log, "TransparentProxyManager::stateChangedCallback. TCP connection failed %{public}@", error.localizedDescription) self.connection.cancel() self.flow.closeReadWithError(error) self.flow.closeWriteWithError(error) case .cancelled: os_log(.debug, log: self.log, "TransparentProxyManager::stateChangedCallback. TCP connection is cancelled %{public}@", "") self.flow.closeReadWithError(nil) self.flow.closeWriteWithError(nil) case .preparing: os_log(.debug, log: self.log, "TransparentProxyManager::stateChangedCallback. TCP connection is preparing %{public}@", "") case .setup: os_log(.debug, log: self.log, "TransparentProxyManager::stateChangedCallback. TCP connection is in setup state %{public}@", "") case .waiting(let error): os_log(.error, log: self.log, "TransparentProxyManager::stateChangedCallback. TCP connection is in waiting state, %{public}@", error.localizedDescription) self.connection.cancel() self.flow.closeReadWithError(error) self.flow.closeWriteWithError(error) default: os_log(.debug, log: self.log, "TransparentProxyManager::stateChangedCallback. State is unknown %{public}@", "") } }
Apr ’25
Reply to How to avoid my local server flows in Transparent App Proxy
private func handleOutgoingTCPData() { os_log(.debug, log: self.log, "handleOutgoingTCPData. handling TCP flow data %{public}@", "") flow.readData { [weak self] data, error in guard let self = self else { return } if let error = error { os_log(.error, log: self.log, "handleOutgoingTCPData. Error on handling TCP flow data: %{public}@", error.localizedDescription) self.connection.cancel() self.flow.closeReadWithError(error) self.flow.closeWriteWithError(error) return } if let data = data, !data.isEmpty { // Check if this is the first packet if self.isFirstPacket { self.isFirstPacket = false // Determine if this is HTTPS (port 443) or HTTP (port 80) let isHttps = self.connection.endpoint.debugDescription.contains(":443") let isHttp = self.connection.endpoint.debugDescription.contains(":80") if isHttps { // Try to extract domain from TLS ClientHello (SNI) if let domain = self.extractSNI(from: data) { self.extractedDomain = domain os_log(.debug, log: self.log, "Extracted SNI domain: %{public}@", domain) // Check if domain is allowed self.checkDomain(domain) { allowed in if !allowed { os_log(.info, log: self.log, "Blocking HTTPS connection to: %{public}@", domain) self.blockConnection = true self.connection.cancel() self.flow.closeReadWithError(nil) self.flow.closeWriteWithError(nil) return } // Domain is allowed, continue with the connection self.sendData(data) } return } else { os_log(.debug, log: self.log, "Could not extract SNI from HTTPS connection, allowing anyway %{public}@", "") } } else if isHttp { // Try to extract Host header from HTTP request if let domain = self.extractHTTPHost(from: data) { self.extractedDomain = domain os_log(.debug, log: self.log, "Extracted HTTP Host: %{public}@", domain) self.checkDomain(domain) { allowed in if !allowed { os_log(.info, log: self.log, "Blocking HTTP connection to: %{public}@", domain) self.blockConnection = true self.connection.cancel() self.flow.closeReadWithError(nil) self.flow.closeWriteWithError(nil) return } // Domain is allowed, continue with the connection self.sendData(data) } return } else { os_log(.debug, log: self.log, "Could not extract Host from HTTP connection, allowing anyway %{public}@", "") } } } // If we reach here, either: // 1. Not the first packet, or // 2. Couldn't extract domain, or // 3. Not HTTP/HTTPS traffic if !self.blockConnection { self.sendData(data) } } else if data?.isEmpty == true { // Empty data but not an error - continue reading self.handleOutgoingTCPData() } else { // No data, likely EOF if !self.blockConnection { os_log(.debug, log: self.log, "No more outgoing data, switching to reading incoming data %{public}@", "") self.handleIncomingTCPData() } } } }
Apr ’25