Bonjour for discovering a specific device's ip

Hi, I'm new to swift programming and right now writing an app for esp8266-controlled lamp device. My lamp is broadcasting it's own IP through bonjour. So all I want is to discover any lamps in my network (http.tcp) and to read name and value. Is there any example of such implementation? All I found so far is old or a lit bit complicated for such simple question. Thanks in advance!
Answered by DTS Engineer in 662482022

Just adding .local to the host name solved the problem.

No it doesn’t. The Bonjour service name and the local DNS name do not need to be related and, even when they are, it’s common for them to be significantly different. For some fun examples of this, see this post.

The only way to get the DNS name of a service is to resolve it.

But it still freezing my app just after service found

Hmmm, I can’t spot the error. Rather than spend time debugging that I created a small test program that illustrates one way to do this. If you paste the code below into a new command-line tool project, it should be able to resolve any service you give it.

Share and Enjoy

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

Code Block
import Foundation
final class BonjourResolver: NSObject, NetServiceDelegate {
typealias CompletionHandler = (Result<(String, Int), Error>) -> Void
@discardableResult
static func resolve(service: NetService, completionHandler: @escaping CompletionHandler) -> BonjourResolver {
precondition(Thread.isMainThread)
let resolver = BonjourResolver(service: service, completionHandler: completionHandler)
resolver.start()
return resolver
}
private init(service: NetService, completionHandler: @escaping CompletionHandler) {
// We want our own copy of the service because we’re going to set a
// delegate on it but `NetService` does not conform to `NSCopying` so
// instead we create a copy by copying each property.
let copy = NetService(domain: service.domain, type: service.type, name: service.name)
self.service = copy
self.completionHandler = completionHandler
}
deinit {
// If these fire the last reference to us was released while the resolve
// was still in flight. That should never happen because we retain
// ourselves on `start`.
assert(self.service == nil)
assert(self.completionHandler == nil)
assert(self.selfRetain == nil)
}
private var service: NetService? = nil
private var completionHandler: (CompletionHandler)? = nil
private var selfRetain: BonjourResolver? = nil
private func start() {
precondition(Thread.isMainThread)
guard let service = self.service else { fatalError() }
service.delegate = self
service.resolve(withTimeout: 5.0)
// Form a temporary retain loop to prevent us from being deinitialised
// while the resolve is in flight. We break this loop in `stop(with:)`.
selfRetain = self
}
func stop() {
self.stop(with: .failure(CocoaError(.userCancelled)))
}
private func stop(with result: Result<(String, Int), Error>) {
precondition(Thread.isMainThread)
self.service?.delegate = nil
self.service?.stop()
self.service = nil
let completionHandler = self.completionHandler
self.completionHandler = nil
completionHandler?(result)
selfRetain = nil
}
func netServiceDidResolveAddress(_ sender: NetService) {
let hostName = sender.hostName!
let port = sender.port
self.stop(with: .success((hostName, port)))
}
func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
let code = (errorDict[NetService.errorCode]?.intValue)
.flatMap { NetService.ErrorCode.init(rawValue: $0) }
?? .unknownError
let error = NSError(domain: NetService.errorDomain, code: code.rawValue, userInfo: nil)
self.stop(with: .failure(error))
}
}
func main() {
let service = NetService(domain: "local.", type: "_ssh._tcp", name: "Fluffy")
print("will resolve, service: \(service)")
BonjourResolver.resolve(service: service) { result in
switch result {
case .success(let hostName):
print("did resolve, host: \(hostName)")
exit(EXIT_SUCCESS)
case .failure(let error):
print("did not resolve, error: \(error)")
exit(EXIT_FAILURE)
}
}
RunLoop.current.run()
}
main()

I work with eskimo code and it runs perfect, but I have one problem.

If you’re referring to this code then that result is expected. If you have multiple devices on a network, each will have its own unique host name. When you resolve that host name you’ll get the set of IP addresses currently associated with that device. If you want to browse for multiple devices, use the NWBrowser class to browse for a service that they advertise. And if you need to remember a device, or a set of devices, record their service names not their host names because the latter is less stable.

Share and Enjoy

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

thank you for the fast answer. I think you didn't understand me correctly. :-) i have some devices, but all have the same serviceType and name:  let service = NetService(domain: "local.", type: "_ssh._tcp", name: "mesh")  my problem is that your code find only one device of them. if I switch the founded device off, I can find the next but I need them all I hope my explanation is understandable

but all have the same serviceType and name

I doubt that. If two peers start out with the same name / service / domain tuple then the second peer to join the network should detect that and rename itself. That’s part of the Bonjour standards [1].

Now, it’s possible that you’re working with a non-compliant accessory but, if you are, there’s not much that the Apple APIs can do to help you out.

It’s more likely, however, that your accessory is working correctly and you’ve just missed the rename. And that’s why I suggested that you browse for services of the specified type. That’ll give you a set of service names, which you can then resolve to map to DNS names (or IP addresses, if that’s your thing).

Share and Enjoy

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

[1] Specifically:

  • RFC 6762 Multicast DNS

  • RFC 6763 DNS-Based Service Discovery

I've try to follow your recommend to use NWBrowser but i got only the results "start function" and "end function" with this code

public func findServices() {
        print("start function")
        let parameter = NWParameters()
        parameter.includePeerToPeer = true
        let browser = NWBrowser(for: .bonjour(type: "_mesh-http._tcp.", domain: "local."), using: parameter)
        browser.stateUpdateHandler = { state in
            switch state {
            case .ready:
                print("the sericeHandler is ready")
            case .failed(let error):
                print("error:", error.localizedDescription)
            default:
                break
            }
        }
        browser.browseResultsChangedHandler = { result, changed in
            print("in handler")
            result.forEach { device in
                device.interfaces.forEach {interface in
                    //ADD THE NAME TO A LIST
                    print(interface.name)
                }
            }
            changed.forEach{change in
                switch change
                {
                case .identical:
                    print("no change")
                    break
                case .added:
                    print("A new result was discovered. ")
                case .removed:
                    print("A previously discovered result was removed.")
                default:
                    break
                }
            }
        }
        print("end function")
        browser.start(queue: .main)
    }

now my Solution. Please leave feedback

--> it's a combination of some answers in this forum

        private func findService(with name:String) {
             let service = NetService(domain: "local.", type: "_mesh-http._tcp.", name: name)
             print("will resolve, service: \(service)")
             BonjourResolver.resolve(service: service) { result in
                 switch result {
                  case .success(let hostName):
                       print("did resolve, host: \(hostName)")
                  case .failure(let error):
                       print("did not resolve, error: \(error)")
            }
        }
    }

    public func findServices()
    {
        let agent = BrowserAgent()
        let browser = NetServiceBrowser()
        browser.delegate = agent
        browser.searchForServices(ofType: "_mesh-http._tcp.", inDomain: "local.")
        browser.schedule(in: RunLoop.main, forMode: .common)
        DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { // give the browser time to brows
                   print("finish")
            for device in agent.getDevices(){
                print(device)
                self.findService(with: device)
            }
                }
        RunLoop.main.run()
    }


final class BonjourResolver: NSObject, NetServiceDelegate {
    typealias CompletionHandler = (Result<(String, Int), Error>) -> Void
    @discardableResult
    static func resolve(service: NetService, completionHandler: @escaping CompletionHandler) -> BonjourResolver {
        precondition(Thread.isMainThread)
        let resolver = BonjourResolver(service: service, completionHandler: completionHandler)
        resolver.start()
        return resolver
    }

    

    private init(service: NetService, completionHandler: @escaping CompletionHandler) {
        // We want our own copy of the service because we’re going to set a
        // delegate on it but `NetService` does not conform to `NSCopying` so
        // instead we create a copy by copying each property.

        let copy = NetService(domain: service.domain, type: service.type, name: service.name)
        self.service = copy
        self.completionHandler = completionHandler
    }

    

    deinit {
        assert(self.service == nil)
        assert(self.completionHandler == nil)
        assert(self.selfRetain == nil)
    }

    private var service: NetService? = nil
    private var completionHandler: (CompletionHandler)? = nil
    private var selfRetain: BonjourResolver? = nil

    private func start() {
        precondition(Thread.isMainThread)
        guard let service = self.service else { fatalError() }
        service.delegate = self
        service.resolve(withTimeout: 3.0)
        selfRetain = self
    }

    func stop() {
        self.stop(with: .failure(CocoaError(.userCancelled)))
    }

    private func stop(with result: Result<(String, Int), Error>) {
        precondition(Thread.isMainThread)
        self.service?.delegate = nil
        self.service?.stop()
        self.service = nil
        let completionHandler = self.completionHandler
        self.completionHandler = nil
        completionHandler?(result)
        selfRetain = nil
    }

    func netServiceDidResolveAddress(_ sender: NetService) {
        let hostName = sender.hostName!
        let port = sender.port
        if let data = sender.txtRecordData() {
            let dict = NetService.dictionary(fromTXTRecord: data)
            print(dict.mapValues { String(data: $0, encoding: .utf8) })
        }
        print("\(hostName)  \(port)")
        self.stop(with: .success((hostName, port)))
    }

    func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
        let code = (errorDict[NetService.errorCode]?.intValue)
            .flatMap { NetService.ErrorCode.init(rawValue: $0) }
            ?? .unknownError
        let error = NSError(domain: NetService.errorDomain, code: code.rawValue, userInfo: nil)
        self.stop(with: .failure(error))
    }
}

class ServiceAgent : NSObject, NetServiceDelegate {
    func netServiceDidResolveAddress(_ sender: NetService) {
        if let data = sender.txtRecordData() {
            let dict = NetService.dictionary(fromTXTRecord: data)
            print("Resolved: \(dict)")
            print(dict.mapValues { String(data: $0, encoding: .utf8) })
        }
    }
}

class BrowserAgent : NSObject, NetServiceBrowserDelegate {
    var currentService:NetService?
     let serviceAgent = ServiceAgent()
    var devices = [String]()
   public func getDevices() -> [String]{
        return devices
    }

    func netServiceBrowser(_ browser: NetServiceBrowser, didFindDomain domainString: String, moreComing: Bool) {
        print("domain found: \(domainString)")
    }
    func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
        print("service found: \(service.name)")
        devices.append(service.name)
    }
}

I make the double search because the result of func netServiceBrowser have no Mac address in the NetService-Object(it's always empty) and I need this. the Solution works but I think it's very awkward

Mac address

Do you mean MAC address here?

It seems like you’re resolving every service that you find. This is something we specifically recommend against because it can trigger a punishing amount of unnecessary network traffic on a large network. In most cases you should only resolve a service if you need to connect to it. Is there a specific reason why you’re resolving every service you find? What other info are you looking for?

Share and Enjoy

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

yes, I need to connect with all devices with this service. I send a httpRequest and get some parameter from all devices. Then I create a dynamic ListView to show DeviceInformations like Devicename, temperature, lightstate...

i find out that all devices have hostname like device.local device-1.local device-2.local .... so I have the best results if search with a for_loop

private func fillHostName()

    {
        let number = 30
        var counter = 0

        self.hostNames.removeAll()
        if newHostNames.count == 0 {
            while counter < number  {
                if counter == 0
                {
                    self.hostNames.append(String("device.local"))
                }
                else
                {
                    self.hostNames.append(String("device-\(counter).local"))
                }
                counter += 1
            }
        }
            else
            {
                self.hostNames = self.newHostNames
            }
    } 


self.hostNames.forEach{ host in
                    self.getMeshInfo(from: host)
                usleep(80000)
}

I know that is "stupid polling" but it works absolut perfekt on very fast.

is there a better way?

i find out that all devices have hostname like device.local, device-1.local, device-2.local

You can’t rely on that. Bonjour accessories have a lot of flexibility in how they choose to rename themselves to avoid collisions.

The standard technique here is to browse for services and then, if necessary, resolve the service as part of the connection process. It’s best to avoid doing the second step for all services. For this reason, many accessories don’t require you to connect to get info like this but instead put useful info in the service’s TXT record.

Consider this:

% dns-sd -B _ipp._tcp. local.
…
Timestamp     A/R    Flags  if Domain  Service Type Instance Name
11:27:46.969  Add        3   6 local.  _ipp._tcp.   Darth Inker
…
^C
% dns-sd -L "Darth Inker" _ipp._tcp. local.
Lookup Darth Inker._ipp._tcp..local.
DATE: ---Tue 14 Dec 2021---
11:28:32.636  ...STARTING...
11:28:32.637  Darth\032Inker._ipp._tcp.local. can be reached at darth-inker.local.:631 (interface 24)
 txtvers=1 qtotal=1 pdl=…,application/postscript,…

_ipp._tcp is the service type used by IPP-capable printers. As you can see, I have one on my network called Darth Inker. When I resolve that I get both the the DNS name and port (darth-inker.local.:631) but also the TXT record. This includes info like the page description language supported by the printer (application/postscript).

If you’re not sure what service is published by your accessory, you can browse for the _services._dns-sd._udp meta service:

% dns-sd -B _services._dns-sd._udp. local.
Browsing for _services._dns-sd._udp..local.
DATE: ---Tue 14 Dec 2021---
11:33:30.189  ...STARTING...
Timestamp     A/R    Flags  if Domain  Service Type Instance Name
…
11:33:30.483  Add        3  24 .       _tcp.local.  _ipp
…^C

If you’re working with a specific accessory, it’s best to run this test with it and your Mac on an isolated network, so you can pick the accessory’s service out of the list.

Share and Enjoy

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

Stumbled across this post and solution and then found that NetService has finally been deprecated as you thought would happen. Wondering if the Network framework has a solution for this now.

I found this SO post: https://stackoverflow.com/questions/60579798/how-to-resolve-addresses-and-port-information-from-an-nwendpoint-service-enum-ca which suggests using a NWConnection and connecting to the service and then discarding the connection after finding the host/port and using that elsewhere. Not great, but likely would work.

Also links to this even older thread https://developer.apple.com/forums/thread/122638 you answered as well.

I'm using a third party C library that handles all the traffic between server/client. I'm adding a bonjour frontend to advertise things which is why I can't use the NWConnection and instead need the host/port to pass in.

Thanks

Wondering if the Network framework has a solution for this now.

No )-:

I found this SO post

That’ll certainly work, but it’s not without its drawbacks. It effectively saves code complexity at the expensive of runtime and network efficiency.

I'm using a third party C library

If you’re in the C world, you might consider switching to the DNS-SD API (<dns_sd.h>), which is not deprecated and supports this functionality (at the cost of even greater code complexity).

Share and Enjoy

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

I was working a DTS tech support incident today and needed a version of this code based on DNS-SD. So I wrote that, and I figured I’d share it here.

Share and Enjoy

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


import Foundation
import Network
import dnssd

final class BonjourResolver: NSObject, NetServiceDelegate {

    typealias CompletionHandler = (Result<(String, Int), Error>) -> Void

    @discardableResult
    static func resolve(endpoint: NWEndpoint, completionHandler: @escaping CompletionHandler) -> BonjourResolver {
        dispatchPrecondition(condition: .onQueue(.main))
        let resolver = BonjourResolver(endpoint: endpoint, completionHandler: completionHandler)
        resolver.start()
        return resolver
    }
    
    private init(endpoint: NWEndpoint, completionHandler: @escaping CompletionHandler) {
        self.endpoint = endpoint
        self.completionHandler = completionHandler
    }
    
    deinit {
        // If these fire the last reference to us was released while the resolve
        // was still in flight.  That should never happen because we retain
        // ourselves in `start()`.
        assert(self.refQ == nil)
        assert(self.completionHandler == nil)
    }
    
    let endpoint: NWEndpoint
    private var refQ: DNSServiceRef? = nil
    private var completionHandler: (CompletionHandler)? = nil
    
    private func start() {
        dispatchPrecondition(condition: .onQueue(.main))
        precondition(self.refQ == nil)
        precondition(self.completionHandler != nil)
        
        do {
            guard
                case .service(name: let name, type: let type, domain: let domain, interface: let interface) = self.endpoint,
                let interfaceIndex = UInt32(exactly: interface?.index ?? 0)
            else {
                throw NWError.posix(.EINVAL)
            }

            let context = Unmanaged.passUnretained(self)
            var refQLocal: DNSServiceRef? = nil
            var err = DNSServiceResolve(
                &refQLocal,
                0,
                interfaceIndex,
                name, type, domain,
                { _, _, _, err, _, hostQ, port, _, _, context in
                    // We ignore the ‘more coming’ flag because we are a
                    // one-shot operation.
                    let obj = Unmanaged<BonjourResolver>.fromOpaque(context!).takeUnretainedValue()
                    obj.resolveDidComplete(err: err, hostQ: hostQ, port: UInt16(bigEndian: port))
                }, context.toOpaque())
            guard err == kDNSServiceErr_NoError else {
                throw NWError.dns(err)
            }
            let ref = refQLocal

            err = DNSServiceSetDispatchQueue(ref, .main)
            guard err == kDNSServiceErr_NoError else {
                DNSServiceRefDeallocate(ref)
                throw NWError.dns(err)
            }
            
            // The async operation is now started, so we retain ourselves.  This
            // is cleaned up when the operation stops in `stop(with:)`.

            self.refQ = ref
            _ = context.retain()
        } catch {
            let completionHandler = self.completionHandler
            self.completionHandler = nil
            completionHandler?(.failure(error))
        }
    }
    
    func stop() {
        self.stop(with: .failure(CocoaError(.userCancelled)))
    }
    
    private func stop(with result: Result<(String, Int), Error>) {
        dispatchPrecondition(condition: .onQueue(.main))

        if let ref = self.refQ {
            self.refQ = nil
            DNSServiceRefDeallocate(ref)
            
            Unmanaged.passUnretained(self).release()
        }
        
        if let completionHandler = self.completionHandler {
            self.completionHandler = nil
            completionHandler(result)
        }
    }
    
    private func resolveDidComplete(err: DNSServiceErrorType, hostQ: UnsafePointer<CChar>?, port: UInt16) {
        if err == kDNSServiceErr_NoError {
            self.stop(with: .success((String(cString: hostQ!), Int(port))))
        } else {
            self.stop(with: .failure(NWError.dns(err)))
        }
    }
}

Thanks Quinn, this compiles and functions in Swift5, would appreciate a Swift6 compatible version. Currently I receive errors when attempting to use logger as below:

browser.stateUpdateHandler = { newState in
            logger.info("Browser did change state, new state: \(newState)")
        }

This produces error "Type of expression is ambiguous without a type annotation". ChatGPT 4 was unable to assist.

Bonjour for discovering a specific device's ip
 
 
Q