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
}
}
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"