Query Regarding NEFilterDataProvider's Hostname Resolution Across Different Browsers

PLATFORM AND VERSION

macOS Development environment: Xcode 15.0, macOS 15.0.1 Run-time configuration: macOS 15.0.1

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

import NetworkExtension
import os.log
import Network

/**
 The FilterDataProvider class handles connections that match the installed rules by prompting
 the user to allow or deny the connections.
 */
class FilterDataProvider: NEFilterDataProvider {
    
    // MARK: NEFilterDataProvider
    
    override func startFilter(completionHandler: @escaping (Error?) -> Void) {
        
        completionHandler(nil)
    }
    
    override func stopFilter(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
        completionHandler()
    }
    
    override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict {
        
        guard let socketFlow = flow as? NEFilterSocketFlow,
              let remoteEndpoint = socketFlow.remoteEndpoint as? NWHostEndpoint,
              let localEndpoint = socketFlow.localEndpoint as? NWHostEndpoint else {
            return .allow()
        }
        
        var hostName: String? = nil
        
        // Attempt to use the URL host for native apps (e.g., Safari)
        if let url = socketFlow.url {
            hostName = url.host
            os_log("URL-based Host: %@", hostName ?? "No host found")
        }
        
        // Fallback: Use remote hostname for third-party browsers like Chrome
        if hostName == nil {
            if #available(macOS 11.0, *), let remoteHostname = socketFlow.remoteHostname {
                hostName = remoteHostname
                os_log("Remote Hostname: %@", hostName ?? "No hostname found")
            } else {
                hostName = remoteEndpoint.hostname
                os_log("IP-based Hostname: %@", hostName ?? "No hostname found")
            }
        }
        
        let flowInfo = [
            FlowInfoKey.localPort.rawValue: localEndpoint.port,
            FlowInfoKey.remoteAddress.rawValue: remoteEndpoint.hostname,
            FlowInfoKey.hostName.rawValue: hostName ?? "No host found"
        ]
        
        // Ask the app to prompt the user
        let prompted = IPCConnection.shared.promptUser(aboutFlow: flowInfo, rawFlow: flow) { allow in
            let userVerdict: NEFilterNewFlowVerdict = allow ? .allow() : .drop()
            self.resumeFlow(flow, with: userVerdict)
        }
        
        guard prompted else {
            return .allow()
        }
        
        return .pause()
    }
    
    // Helper function to check if a string is an IP address
    func isIPAddress(_ hostName: String) -> Bool {
        var sin = sockaddr_in()
        var sin6 = sockaddr_in6()
        if hostName.withCString({ inet_pton(AF_INET, $0, &sin.sin_addr) }) == 1 {
            return true
        } else if hostName.withCString({ inet_pton(AF_INET6, $0, &sin6.sin6_addr) }) == 1 {
            return true
        }
        return false
    }
}
Answered by DTS Engineer in 811527022
we need to drop network flows based on the hostname.

That’s not going to work reliably. Our system can only give you the information it has. If a third-party app uses the resolve-then-connect approach — something we actively recommend against — your filter won’t get a DNS name. See this thread for more on this.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

we need to drop network flows based on the hostname.

That’s not going to work reliably. Our system can only give you the information it has. If a third-party app uses the resolve-then-connect approach — something we actively recommend against — your filter won’t get a DNS name. See this thread for more on this.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Query Regarding NEFilterDataProvider's Hostname Resolution Across Different Browsers
 
 
Q