NWBrowser not working for services named _ssh._tcp

I'm using NWBrowser to connect to devices on my local network. One of the local devices broadcasts a service named "_mik._tcp." the other is "_ssh._tcp." I have no issues connecting to "_mik._tcp." but "_ssh._tcp." never connects.

Is there some reason why iOS would be preventing "_ssh._tcp." connections?

I have both services listed in the info.plist and have implemented the following NWParameters:

let parameters = NWParameters()
parameters.allowLocalEndpointReuse = true
parameters.acceptLocalOnly = true
parameters.allowFastOpen = true
parameters.multipathServiceType = .aggregate

browser = NWBrowser(for: .bonjour(type: "_ssh._tcp.", domain: "local."), using: parameters)

However, with "_ssh._tcp." I never get past the "preparing" state:

self.netConnect?.stateUpdateHandler = { (newState) in
...

"_mik._tcp." receives both "preparing" and "ready" states almost instantly.

Using the Discovery app on my Mac I can see the services listed, however _ssh.tcp has the description SSH Remote Login Protocol which I think is the root of the issue.

I'm using NWBrowser … but _ssh._tcp. never connects.

I’d like to clarify what you mean by “connects” here. There are three steps in a product like this:

  1. Browse for services

  2. Select a service

  3. Open a TCP connection to that service

Your use of “connects” suggests that you’re having a problem with step 3, but the rest of your post suggests that you’re having a problem with step 1. Which is it?

Share and Enjoy

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

Sorry for the confusion. I assume it is step 3, that the connection is made, but it is still in the preparing state.

self.netConnect?.stateUpdateHandler = { (newState) in

With _ssh._tcp. newState never gets past preparing whereas the other service _mik._tcp. gets to ready

It’s possible that you’re hitting an known issue in how we resolve Bonjour connections (r. 101804570). Does this reproduce in the simulator? How about the latest iOS 16.3 beta (20D5024e)?

Share and Enjoy

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

Unfortunately, 16.3 doesn't resolve the issue. The WebRTC Library used in this app we are unable to test in the Simulator at this time (we are working on making that happen).

This is a typical infrastructure Wi-Fi network, right? So not peer-to-peer Wi-Fi? And it has access to the wider Internet?

I just cooked up a trivial test app to exercise this and it’s working for me. The code is below if you’re interested (sorry it’s so ugly).

In the code snippet in your original post you configure parameters really strangely. If you do what I did, just use .tcp, does that improve things?

Share and Enjoy

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


import UIKit
import Network

final class MainViewController: UITableViewController {

    struct Service {
        var name: String
        var endpoint: NWEndpoint
        var interface: NWInterface?
        var connection: NWConnection?
    }
    
    let browser: NWBrowser = .init(for: .bonjour(type: "_ssh._tcp.", domain: "local."), using: .tcp)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.browser.stateUpdateHandler = { state in
            print(state)
        }
        self.browser.browseResultsChangedHandler = { services, _ in
            self.services = services.map { result in
                guard case .service(name: let name, type: _, domain: _, interface: let interface) = result.endpoint else { fatalError() }
                return Service(name: name, endpoint: result.endpoint, interface: interface, connection: nil)
            }
            self.tableView.reloadData()
        }
        self.browser.start(queue: .main)
    }
    
    var services: [Service] = []
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        services.count
    }

    func sync(cell: UITableViewCell, at indexPath: IndexPath) {
        let service = services[indexPath.row]
        cell.textLabel!.text = "\(service.name) [\(service.interface?.name ?? "-")]"
        let status: String
        switch service.connection?.state {
        case nil: status = "-"
        case .setup?: status = "setup"
        case .waiting(_)?: status = "waiting"
        case .preparing?: status = "preparing"
        case .ready?: status = "ready"
        case .failed(_)?: status = "failed"
        case .cancelled?: status = "cancelled"
        @unknown default: status = "?"
        }
        cell.detailTextLabel!.text = status
    }

    func syncCell(at indexPath: IndexPath) {
        guard let cell = self.tableView.cellForRow(at: indexPath) else { return }
        self.sync(cell: cell, at: indexPath)
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        self.sync(cell: cell, at: indexPath)
        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if let connection = self.services[indexPath.row].connection {
            self.services[indexPath.row].connection = nil
            connection.cancel()
        } else {
            let endpoint = self.services[indexPath.row].endpoint
            let connection = NWConnection(to: endpoint, using: .tcp)
            self.services[indexPath.row].connection = connection
            connection.stateUpdateHandler = { newState in
                self.syncCell(at: indexPath)
            }
            connection.start(queue: .main)
        }
        self.syncCell(at: indexPath)
        tableView.deselectRow(at: indexPath, animated: true)
    }
}

Thank you for your reply. the failure I am seeing is a bit deeper in the process, not with the NWBrowser, but with the NWConnection I am ultimately using to resolve the ipaddress.

Below, you will see my extra code that I added to the sample you provided above. I use the changes to create a NWConnection

Is there a simpler way to get the ipaddress?

   self.browser.browseResultsChangedHandler = { services, changes in

            self.services = services.map { result in

                guard case .service(name: let name, type: _, domain: _, interface: let interface) = result.endpoint else { fatalError() }

                return Service(name: name, endpoint: result.endpoint, interface: interface, connection: nil)

            }

            print("SERVICES \(self.services)")

            for change in changes {

                if case .added(let added) = change {

                    switch added.endpoint {

                    case .service(name: let name, type: let type, domain: let domain, interface: let interface):

                        self.netConnection = NWConnection(to: .service(name: name, type: type, domain: domain, interface: interface), using: .tcp)

                        print("netConnection stateUpdateHandler start")

                        self.netConnection?.stateUpdateHandler = { (newState) in

                            //with _ssh._tcp. this new state below never gets past `preparing`. With the other type, it goes to `ready` then I get the host

                            print("netConnection newState \(newState)")

                            switch (newState) {

                            case .ready:

                                guard let currentPath = self.netConnection?.currentPath else { return }

                                if let endpoint = currentPath.remoteEndpoint {

                                    switch endpoint {

                                    case .hostPort(host: let host, port: _):

                                        switch host {

                                        case .ipv4(_):

                                            //this is where I get the ipaddress ...

                                            print("host ipv4: \(host.debugDescription)")

                                        case .ipv6(_):

                                            //this is where I get the ipaddress ...

                                            print("host ipv6: \(host.debugDescription)")

                                        default:

                                            print("host: not found")

                                            break

                                        }

                                    default:

                                        break

                                    }

                                }

                            default:

                                break

                            }

                        }

                    default:

                        break

                    }

                }

            }

            self.netConnection?.start(queue: .main)

Is there a simpler way to get the ipaddress?

Yes, and no (-:

The code I posted will run a connect, but only if you tap on the cell that lists the service. If you do that in your environment, do you see the connection go through?

What you’re doing, resolving every service you discover, is most definitely a Bonjour anti-pattern because on a large network it will generate excessive network traffic. That’s why our Bonjour APIs don’t provide IP address information as part of the browse operation.

Why are you trying to resolve every service you find? What do you plan to do with that IP address info?

Share and Enjoy

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

NWBrowser not working for services named _ssh._tcp
 
 
Q