NWConnection not connecting, and not receiving errors

How to handle a connection timing out?

// `endpoint` was obtained from a bonjour browser
let connection = NWConnection(to: endpoint, using: .tcp)
connection.stateUpdateHandler = { [self] newState in
    print("newState: \(newState)")
}
connection.start(queue: .main)

Log:

newState: preparing
nw_socket_handle_socket_event [C1.1.4.1:1] Socket SO_ERROR [60: Operation timed out]
... several more times

The stateUpdateHandler function is never called again.

Answered by DTS Engineer in 750633022

How to handle a connection timing out?

NWConnection does not, in general, implement timeouts. If you want that, you have to implement it yourself using a timer.

I’m curious about your setup though. When things fail in this way, is it because the server is not responding? Or is the server up’n’running but the connection is failing for some reason?

Also, what platform? And what version of that platform?

Share and Enjoy

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

Accepted Answer

How to handle a connection timing out?

NWConnection does not, in general, implement timeouts. If you want that, you have to implement it yourself using a timer.

I’m curious about your setup though. When things fail in this way, is it because the server is not responding? Or is the server up’n’running but the connection is failing for some reason?

Also, what platform? And what version of that platform?

Share and Enjoy

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

Huh... I assumed any error would translate into a stateUpdateHandler call with a .failed(error) case. But okay, I can add a timeout.

There is a listener on the other device - that's where the endpoint is coming from. I'm not sure why they can't connect to each other, if they can see one another via Bonjour. The participants are: a simulator running iOS 16.2, and an iPhone running 16.3.1.

I can add a timeout.

To be clear, adding a timeout only make sense if this network operation is invisible to the user. For a visible operation, it’s better to leave the connection request running and provide some UI for the user to cancel it.

I'm not sure why they can't connect to each other, if they can see one another via Bonjour.

There are two Bonjour operations involved here, browse and resolve. It’s possible, albeit rare, for the browse to work and the resolve to fail. It’s also possible that the resolve worked but the TCP connection is failing.

In situations like this I usually use command-line tools to test the resolve and connect operations separately. For example:

% dns-sd -B _ssh._tcp. local.
Browsing for _ssh._tcp..local.
DATE: ---Sun 16 Apr 2023---
16:54:37.008  ...STARTING...
Timestamp     A/R    Flags  if Domain     Service Type     Instance Name
16:54:37.009  Add        3   5 local.     _ssh._tcp.       Slimey
^C
% dns-sd -L Slimey _ssh._tcp. local.
Lookup Slimey._ssh._tcp..local.
DATE: ---Sun 16 Apr 2023---
16:54:46.238  ...STARTING...
16:54:46.239  Slimey._ssh._tcp.local. can be reached at Slimey.local.:22 (interface 5) Flags: 1
…
^C
% nc Slimey._ssh._tcp.local. 22
nc: getaddrinfo: nodename nor servname provided, or not known
% nc Slimey.local 22           
SSH-2.0-OpenSSH_9.0
^C

The first command browsers for services (SSH servers in my case), the second resolves a specific service to a DNS name and port, and the third connects to that DNS name and port.

Share and Enjoy

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

The timeout issue is now resolved. The connection class has an optional timeout property, so it can be cancelled either by timeout, or manually.

The second issue of not being able to connect is still open, though. The IP address resolved by the Bonjour is link-local, e.g. fe80::908d:59ff:fe60:8858%en9. Connecting to this address fails with "no route to host". If I ask the device for its own address (using the getifaddrs function), it returns a fd60:... address, which I can connect to.

The address is resolved like this:

func connectionReady(_ connection: NWConnection) throws {
    guard let remoteEndpoint = connection.currentPath?.remoteEndpoint else {
        throw NWError.posix(.EADDRNOTAVAIL)
    }

    // Connected: get address, and sync with peer
    switch remoteEndpoint {
    case .hostPort(let host, _):
        var addr = "\(host)"
        print(addr) // prints fe80:...%en9
        doSync(with: addr)
    default:
        throw NWError.posix(.EDESTADDRREQ)
    }
}

The second issue of not being able to connect is still open, though.

I’m confused. When browsing for Bonjour services the NWBrowser vends endpoints of the form NWEndpoint.service(name:type:domain:interface:). You can pass such an endpoint directly to NWConnection. How are IP addresses coming into this?

Share and Enjoy

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

Sorry, I should have provided more context. My API (doSync) needs an IP address. So I pass the endpoint to an NWConnection to resolve the address. But the address is not directly reachable:

% ping6 fe80::908d:59ff:fe60:8858
ping6: UDP connect: No route to host

Interestingly, adding the interface suffix works.

% ping6 fe80::908d:59ff:fe60:8858%en9
PING6(56=40+8+8 bytes) fe80::908d:59ff:fe60:88a7%en9 --> fe80::908d:59ff:fe60:8858%en9
16 bytes from...

But my API doesn't understand this address format.

You bumping into a fundamental limit of link-local addresses: They don’t make any sense without the interface scope.

My API (doSync) needs an IP address.

OK, lemme see if I have the big picture right:

  1. You use NWBrowser to browse for services.

  2. It vends a .service(…) endpoint.

  3. You connect to that using NWConnection.

  4. Once you’re connected you get the remote peer IP address from remoteEndpoint.

  5. You pass that to this to your doSync API.

  6. It fails to connect.

Is that right?

If so, do you make the connection in step 3 solely to get the remote peer IP address in step 4? Or do you use that connection for other purposes?

Share and Enjoy

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

Yes, the summary is correct. The purpose of the connection is solely to obtain the remote endpoint's IP address. But I'm considering having the remote device send its "real" address over. The connection has a protocol framer already, so it wouldn't be a big change.

The team that owns the sync API has made some changes to accept and parse the interface. That makes it work, some of the time. Most of the time, sync times out or gets "connection refused".

Will the doSync API accept a DNS name?

Share and Enjoy

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

NWConnection not connecting, and not receiving errors
 
 
Q