NWConnection endPoint info duplicated

I'm not native to networking so maybe I'm misunderstanding how endPoint information is gathered.

Device_A is browsing and discovers Device_B or the other way around, it doesn't matter because they will both discover each other and send data to open a connection. Because the Network framework does not resolve ipAddresses, when a connection is first made I use either the remoteEndPoint (connection.currentPath?.remoteEndpoint // fe80::9821:7fff:fcea:74c4%awdl0.10059) or the endPoint description (connection.endpoint.debugDescription // myApp (2)._myApp._tcplocal.) as a uniqueID for the connection.

I send the data and some other info across the wire, if successful I then place either endPoint inside an ivar dictionary as the key with another value so that I know which device to map a response to once a device responds back.

Right now I only have 2 devices connected, the issue is when I receive a response from Device_B, I'm actually getting the incorrect endPoint information. For some reason I keep getting the endPoint info from Device_A back. It seems like the same endPoint information is getting sent twice. Once to Device_A and then again to Device_B. This exact same thing occurs on Device_B but in reverse. I'm confused as to why this is happening.

For example Device_A first discovers itself, the remoteEndPoint is *fe80::9821:7fff:fcea:74c4%awdl0.10059*, it sends the data. When Device_A receives its own message, I filter it out using the userId and I see the same endPoint info. But when Device_A discovers Device_B, the remoteEndPoint is *fe80::9821:7fff:fcea:74c4%awdl0.27788*. When I receive a response from Device_B, the endPoint information is showing the first one from Device_A *fe80::9821:7fff:fcea:74c4%awdl0.10059*. The same remoteEndPoint info is duplicated. The same exact thing is happening if I use endpoint.debugDescription. This issue occurs on both devices.

NWBrowser:

Code Block
browser.browseResultsChangedHandler = { (results, changes) in
for change in changes {
switch change {
case .added(let browseResult):
switch browseResult.endpoint {
case .service(let name, let type,_,_):
let connection = PeerConnection(to: browseResult.endpoint)
// ...
}


PeerConnection:

Code Block
var connection: NWConnection?
init(to endPoint: NWEndpoint) {
// tcpOptions ...
// params ...
// initialize connection and delegate that sends out data
connection.stateUpdateHandler = { (nwConnectionState) in
case .ready:
self.delegate.sendOutgoing(connection)
}


Send Data:

Code Block
var dict = [String:String]()
var endPointArr = [String]()
func sendOutgoing(_ connection: NWConnection) {
let endPoint = connection.currentPath?.localEndpoint?.debugDescription
or
let endPoint = connection.currentPath?.remoteEndpoint?.debugDescription
// encode the endPoint and currentUserId with some other info, then send it across the wire, if successful make the endPoint a key inside a dictionary and add it to an array to keep track of what endPoints were received
connection.send(content: encodedData, contentContext: context, isComplete: true, completion: .contentProcessed({ [weak self](error) in {
if let error = error { return }
self?.dict[endPoint] = someUniqueValue
self?.endPointArr.append(endPoint)
}))
}

Receiving a response

Code Block
connection.receive(minimumIncompleteLength: 1, maximumLength: 65535) { [weak self](data, context, isComplete, error) {
if let err = error { return }
if let data = data, !data.isEmpty {
self?.received(data)
}
}
func received(_ data) {
guard let retrievedData = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? MyModel else { return }
guard let endPoint = retrievedData.endPoint as? String, let userID = retrievedData.userId as? String else { return }
print(endPoint) // fe80::9821:7fff:fcea:74c4%awdl0.10059
if userID == Auth.auth().currentUser?.uid {
dict[endPoint] = nil
return
}
endPointArr.forEach { (endPoint) in
print(endPoint) // prints both fe80::9821:7fff:fcea:74c4%awdl0.10059 and fe80::9821:7fff:fcea:74c4%awdl0.27788
}
// this never runs because the key just got set to nil above because Device_B has the same endPoint info
for (key, value) in dict where key == endpoint {
print("key=\(key) : value=\(value)") // someUniqueValue
// show response that this is a response from whichever device has this endPoint
break
}
}



[1]: https://developer.apple.com/forums/thread/129644

Accepted Reply

I suspect that you’re missing the forest for the trees here. In your received(:_) method you’re trying to use information in the message you received to identify the sender. That’s problematic because that data is under the control of the sender, which means that they could pass you anything.

Based on your other post I presume you’re using WebSocket for this. In that case the underlying transport is TCP, which means that you can identify the sender by the connection itself. It seems you have a one-to-one relationship between PeerConnection and NWConnection, and so your receive(_:) method can use self to work out who it’s talking to.



Oh, and I’m concerned about line 9 of your first snippet. Are you actually connecting to every service you discover over Bonjour? That’s generally considered bad form because the act of resolving a service and then connecting to it is quite expensive (especially for dynamic interfaces like peer-to-peer Wi-Fi). It’s better to come up with a design when you only connect to relevant services. For example:
  • You might present the user with a list of services and have them choose one.

  • You might remember the user’s previous choice and automatically connect to that.

  • You might use metadata about the service (in the TXT record) to filter for relevant ones.

If you can post more details about the UI you’re looking for, I can offer more concrete suggestions (it’d make sense to put that in a new thread and leave this one to concentrate on the connection identification issue).

Share and Enjoy

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

Replies

I suspect that you’re missing the forest for the trees here. In your received(:_) method you’re trying to use information in the message you received to identify the sender. That’s problematic because that data is under the control of the sender, which means that they could pass you anything.

Based on your other post I presume you’re using WebSocket for this. In that case the underlying transport is TCP, which means that you can identify the sender by the connection itself. It seems you have a one-to-one relationship between PeerConnection and NWConnection, and so your receive(_:) method can use self to work out who it’s talking to.



Oh, and I’m concerned about line 9 of your first snippet. Are you actually connecting to every service you discover over Bonjour? That’s generally considered bad form because the act of resolving a service and then connecting to it is quite expensive (especially for dynamic interfaces like peer-to-peer Wi-Fi). It’s better to come up with a design when you only connect to relevant services. For example:
  • You might present the user with a list of services and have them choose one.

  • You might remember the user’s previous choice and automatically connect to that.

  • You might use metadata about the service (in the TXT record) to filter for relevant ones.

If you can post more details about the UI you’re looking for, I can offer more concrete suggestions (it’d make sense to put that in a new thread and leave this one to concentrate on the connection identification issue).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
@eskimo ok thanks for the answer. I'll start a new thread about the issue that you just identified. As far as this one, how do I know who is talking to who by using self inside the receive(_:) method?

Once a sender sends a message, I used these for the receiver to identify the sender:

Code Block
let endPointDescription = connection.currentPath?.localEndpoint?.debugDescription ... encode then send
let remoteEndPoint = connection.currentPath?.remoteEndpoint?.debugDescription ... encode then send
// they both get sent together


This works fine when detecting when either end is cut off, for example if Device_A is connected to Device_B, if Device_B goes to the background the connection is cut and Device_A immediately knows about it. I haven't had any problems even when using 3 devices and 2 simulators. They all will know that Device_B has left the building and respond accordingly.
So, to be clear, you’re using WebSocket over TCP, right?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Yes, TCP and Websocket, here is the send message:

Code Block
let info = // some String info
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: info, requiringSecureCoding: true) else {
return
}
let message = NWProtocolWebSocket.Metadata(opcode: .text)
let context = NWConnection.ContentContext(identifier: "send", metadata: [message])
connection.send(content: data, contentContext: context, isComplete: true, completion: .contentProcessed({ (error) in
if let error = error { print("\(error.localizedDescription)"); return }
print("Success")
}))


The NWListener, NWBrowser, and NWConnection all are initialized with let tcpOptions = NWProtocolTCP.Options() inside their NWParameters.
TCP has the concept of connections, where a TCP connection is uniquely identified by the local and remote endpoints, that is, the tuple of local IP / local port / remote IP / remote port. This concept is reflected at the NWConnection level. If you receive traffic on an NWConnection you know it came from the underlying TCP connection. There’s no possibility of you say, receiving one message from remote IP X and then, on the same connection, another message from remote IP Y.

This means that you don’t need to authenticate messages, you can simply authenticate connections. So:
  • For an outgoing connection you don’t need to do anything special to identify the remote endpoint because you specified that when you opened the connection.

  • For an incoming connection you can identify the remote endpoint in your new connection handler.

Having said that, identifying a connection via its endpoint is not great because a given host will have many different IP addresses and that list will change regularly. It’s better to identify a connection using a more stable value. If you protocol supports authentication, you can usually get that stable value from the authentication process. For example, in the classic user name and password scenario, the user name identifies the connection. If not, you can come up with a custom identification scheme.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
I just typed a very descriptive response to the other question that you was helping me out with and I lost it all because it said that I was unauthorized to respond. That is extremely frustrating.

I wonder are the 3 new problems that I encountered in my other question based on your last response to that question related to what you said above. From what you are saying my setup is incorrect and that could be the problem right there.

Should I start new thread or open a TSI?
@eskimo

You're correct. I'm doing something wrong. What i thought was working upon further testing isn't working and I think this may be causing the issue that I'm having in the other question.