Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

NWBrowser scan for arbitrary Bonjour Services with Multicast Entitlement ?!

Dear Girls, Guys and Engineers.

I'm currently building a Home Network Scanner App for People which want to know which Bonjour Devices are in her/his Home Network environment. From an older Question I got the answer, that I need an Entitlement to do this.

I started to work on the App and requested the Multicast Entitlement from Apple. They gave me the Entitlement for my App and now I'm trying to discover all devices in my Home Network but I got stuck and need Help.

I only test direct on device, like the recommendation. I also verified that my app is build with the multicast entitlement there where no problems. My problem is now, that is still not possible to discover all Bonjour services in my Home Network with the Help of the NWBrowser.

Can you please help me to make it work ?

I tried to scan for the generic service type:

let browser = NWBrowser(for: .bonjour(type: "_services._dns-sd._udp.", domain: nil), using: .init())

but this is still not working even tough I have the entitlement and the app was verified that the entitlement is correctly enabled

if I scan for this service type, I got the following error:

[browser] nw_browser_fail_on_dns_error_locked [B1] Invalid meta query type specified. nw_browser_start_dns_browser_locked failed: BadParam(-65540)

So what's the correct way now to find all devices in the home network ?

Thank you and best regards Vinz

Answered by DTS Engineer in 840436022

I’m gonna focus on iOS for the moment, because I’m seeing some results on macOS that I can’t readily explain.

On iOS you need the Multicast Network capability and the NSLocalNetworkUsageDescription property in your Info.plist.

Running the code below I see output like this:

browser will start
browser did find, service: <NSNetService 0x15c18ac00> . _tcp.local. _afpovertcp -1
browser did find, service: <NSNetService 0x15c18ac00> . _tcp.local. _ipp -1
browser did find, service: <NSNetService 0x15c18ac00> . _tcp.local. _ipps -1
browser did find, service: <NSNetService 0x15c18ac00> . _tcp.local. _ipp-tls -1
browser did find, service: <NSNetService 0x15c18ac00> . _tcp.local. _http -1
…

This seems to be working as expected, that is, it’s reporting all the service types discovered on the local network.

You wrote:

however calls to resolve for each service, result in didNotResolve.

Right, because you can’t resolve a service type. Rather, you’re expected to get the type from each result (via the name property) and start new browser for services of that type. That’ll results in services that you can then resolve. Consider:

(lldb) po service.name
_afpovertcp
(lldb) po service.type
_tcp.local.
(lldb) po service.domain 
.

Once you get this working on iOS, lemme know and we can talk about the macOS situation.

Share and Enjoy

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


@Observable
final class AppModel: NSObject, NetServiceBrowserDelegate {

    var browserQ: NetServiceBrowser? = nil
    
    func startStop() {
        if let browser = self.browserQ {
            self.browserQ = nil
            self.stop(browser: browser)
        } else {
            self.browserQ = self.start()
        }
    }
    
    private func start() -> NetServiceBrowser {
        print("browser will start")
        let browser = NetServiceBrowser()
        browser.delegate = self
        browser.searchForServices(ofType: "_services._dns-sd._udp.", inDomain: "local.")
        return browser
    }
    
    private func stop(browser: NetServiceBrowser) {
        print("browser will stop")
        browser.delegate = nil
        browser.stop()
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
        print("browser did find, service: \(service)")
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber]) {
        print("did not search, error: \(errorDict)")
    }
}
Accepted Answer

Browsing for the _services._dns-sd._udp. service with NWBrowser would be an Enhancement Request against Network Framework. Please follow up with your Feedback ID. A workaround would be to use the DNSServiceBrowse API.

Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Accepted Answer

Matt and I had a good chat with the Network framework and Bonjour teams about this yesterday, so I thought I’d elaborate on the above a little.

First up, you can’t use NWBrowser to browse for services. This isn’t an arbitrary restriction but a result of the way that NWBrowser is structured. Specifically, the browse operation returns endpoints and there’s no way to represent a service type as an endpoint.

That leaves you with either NSNetService or the DNS-SD API (<dns_sd.h>). Given that this is a low-level thing, and NSNetService is now deprecated, I recommend DNS-SD. That's not the easiest API to use but you can get a head start by looking at the DNSSDObjects sample code.

Let us know if you hit any snags.

Share and Enjoy

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

Hi Matt and Quinn,

Thank you for your fast reply. This light up things for me, i will try to get the service list via dnssd framework. To get the Endpoints I can then use NWBrowser. Just for clarification, the entitlement is still needed to do this or for iOS 14+ ? I used NSNetservice in the past and it was possible to get service list without the entitlement.

Thank you both

Just for clarification, the entitlement is still needed to do this or for iOS 14+?

Yes.

I used NSNetservice in the past and it was possible to get service list without the entitlement.

Correct. This is a policy change added as part of the local network privacy effort.

Share and Enjoy

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

Hi Quinn,

I've been experimenting with NetServiceBrowser to try to get a list of all local Bonjour broadcasts (for a developer app communicating with local devices). I have the com.apple.developer.networking.multicast entitlement and I have confirmed that Xcode is adding the entitlement to the app bundle.

private let serviceBrowser = NetServiceBrowser()
serviceBrowser.searchForServices(ofType: "_services._dns-sd._udp", inDomain: "local.")
serviceBrowser.schedule(in: RunLoop.main, forMode: .common)

Running in MacCatalyst under macOS 15.5, the NetServiceDelegate only gets an error, no services are found.

didNotSearch: ["NSNetServicesErrorDomain": 10, "NSNetServicesErrorCode": -72008]

On iOS 18, the NetServiceDelegate didFind function gets called for each local service, however calls to resolve for each service, result in didNotResolve. Calling the exact same code with a known service, such as "_airplay._tcp" works perfectly, resulting in the delegate receiving didFind for the service and subsequent netServiceDidResolveAddress and didUpdateTXTRecord call backs.

I have tried NWBrowser instead, but then found one of your posts stating that this API does not support searching for all available broadcasts. I also tried to adapt your BonjourResolver code but with that I only got a -65540: BadParam error when passing the _services._dns-sd._udp name.

I'm at a loss, and wonder if you know of any way I can get this to work under macOS 15 and iOS 18 ?

Many thanks

I’m gonna focus on iOS for the moment, because I’m seeing some results on macOS that I can’t readily explain.

On iOS you need the Multicast Network capability and the NSLocalNetworkUsageDescription property in your Info.plist.

Running the code below I see output like this:

browser will start
browser did find, service: <NSNetService 0x15c18ac00> . _tcp.local. _afpovertcp -1
browser did find, service: <NSNetService 0x15c18ac00> . _tcp.local. _ipp -1
browser did find, service: <NSNetService 0x15c18ac00> . _tcp.local. _ipps -1
browser did find, service: <NSNetService 0x15c18ac00> . _tcp.local. _ipp-tls -1
browser did find, service: <NSNetService 0x15c18ac00> . _tcp.local. _http -1
…

This seems to be working as expected, that is, it’s reporting all the service types discovered on the local network.

You wrote:

however calls to resolve for each service, result in didNotResolve.

Right, because you can’t resolve a service type. Rather, you’re expected to get the type from each result (via the name property) and start new browser for services of that type. That’ll results in services that you can then resolve. Consider:

(lldb) po service.name
_afpovertcp
(lldb) po service.type
_tcp.local.
(lldb) po service.domain 
.

Once you get this working on iOS, lemme know and we can talk about the macOS situation.

Share and Enjoy

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


@Observable
final class AppModel: NSObject, NetServiceBrowserDelegate {

    var browserQ: NetServiceBrowser? = nil
    
    func startStop() {
        if let browser = self.browserQ {
            self.browserQ = nil
            self.stop(browser: browser)
        } else {
            self.browserQ = self.start()
        }
    }
    
    private func start() -> NetServiceBrowser {
        print("browser will start")
        let browser = NetServiceBrowser()
        browser.delegate = self
        browser.searchForServices(ofType: "_services._dns-sd._udp.", inDomain: "local.")
        return browser
    }
    
    private func stop(browser: NetServiceBrowser) {
        print("browser will stop")
        browser.delegate = nil
        browser.stop()
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
        print("browser did find, service: \(service)")
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber]) {
        print("did not search, error: \(errorDict)")
    }
}

Hi Quinn,

Thanks for your very helpful and clear response. Yes, I did have the NSLocalNetworkUsageDescription property set in the Info.plist.

After much head scratching, I am glad to report that I have now discovered the issue. I had the NSBonjourServices property in the plist, with a number of services listed with which I interacted with elsewhere in the app, such as _hap._tcp, as instructed in Getting Started with Bonjour.

Mysteriously, if that property contains any services, then searchForServices fails for _services._dns-sd._udp. Either deleting that property from the Info.plist, or just deleting all array elements of that property fixes the problem. With that change all searches are subsequently possible. It took me a while to realize that because Xcode doesn't seem to replace the Info.plist in a build when it is changed, but only after clearing the build folder... sigh.

Regarding the resolution of services, Quinn wrote:

Right, because you can’t resolve a service type. Rather, you’re expected to get the type from each result (via the name property) and start new browser for services of that type. That’ll results in services that you can then resolve.

Many thanks, now if I correctly test the results of didFind, and launch a separate NetServiceBrowser for each one, I was then able to resolve individual text records.

With that surprise discovery and with your tip on service resolution, the code is now working well on both macCatalyst and iOS.

Many thanks ! Guy

I’m glad you got this sorted out.

Mysteriously, if that property contains services, then searchForServices fails for _services._dns-sd._udp.

I’d appreciate you filing a bug about that. The current behaviour is not at all helpful.

Please post your bug number, just for the record.


Finally, now that you have the basics sorted out, I encourage you to move off NSNetService. It’s been deprecated for a while.

AFAICT _services._dns-sd._udp is not supported by NWBrowser, which means using using DNS-SD. That’s quite tricky to use correctly. The DNSSDObjects sample code shows how to do it, but that’s in Objective-C and Swift adds some extra challenges. For an example of how to call the DNS-SD API from Swift, see this post.

IMPORTANT This calls a completely different API — DNSServiceResolve rather than DNSServiceBrowse — but the basic ideas are the same in both cases.

Share and Enjoy

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

Many thanks Quinn, bug has been filed FB17761123

So the response came quite quickly, but was disappointing:

Engineering has provided the following information regarding this issue: On a Mac, specifying a service in NSBonjourServices, allows only that service to be accessed on the local network. No NSBonjourServices means allow all services (with user consent). This sounds like its is working properly. You can either add "_services._dns-sd._udp." to the NSBonjourServices, or remove it entirely

I have responded explaining that this behavior is not documented, and works differently to iOS and iPadOS, which is particularly troubling for a Catalyst app which shares the Info.plist between all those environments.

NWBrowser scan for arbitrary Bonjour Services with Multicast Entitlement ?!
 
 
Q