P2P networking between Apple devices

I'm working on an app that does peer-to-peer communication between Apple devices. As far as I understand, the Network framework is a good choice for this. I have something that works, but I'm curious about the details of how this works and if I might somehow optimize this.

My current understanding is that the best connection I can get between two devices is over AWDL. Is this true? If so, does Network use this? Can I ask it to use it preferentially? What kind of bandwidth and latency should I expect out of this, and are there any drawbacks to using it like power usage or transport limitations?

If both devices are on the same LAN, I assume they can also talk to each other over Wi-Fi (or a wired connection if both are plugged in, I guess). If I use Bonjour service discovery, is this what I will be getting? What does Network do if the LAN network does not perform well? Will it swap the underlying connection if it figures out there is something better?

I am not tied to any particular API or transport protocol, so any input on tradeoffs between ease of implementation/performance/reliability/whatever would be welcome :)

My current understanding is that the best connection I can get between two devices is over AWDL peer-to-peer Wi-Fi.

First up, AWDL is an implementation detail, not an API. We generally refer to this feature as peer-to-peer Wi-Fi. Notably, our original peer-to-peer Wi-Fi implementation was not based on AWDL at all!

Is this true?

It depends on a bunch of context. If both devices are on the same infrastructure Wi-Fi, the best connection is likely to be via that infrastructure Wi-Fi.

If so, does Network use this?

Yes, although you must opt in by setting the includePeerToPeer property.

Can I ask it to use it preferentially?

Not with Network framework. And even if you couldn’t, you wouldn’t want to, because if infrastructure Wi-Fi works then it’s generally better.

What kind of bandwidth and latency should I expect out of this

As with anything Wi-Fi, it depends on a bunch of factors, including the device types and their physical relationship. You’ll need to explore this for yourself.

and are there any drawbacks to using it like power usage or transport limitations?

The main drawback is that it enabling peer-to-peer Wi-Fi impacts on the performance of infrastructure Wi-Fi. That’s why it’s not on by default.

If both devices are on the same LAN, I assume they can also talk to each other over Wi-Fi (or a wired connection if both are plugged in, I guess).

That’s often true, but not always. It’s common for Wi-Fi networks to prohibit STA-to-STA trafic, for example, in your local coffee shop. You might even see that sort of thing on Ethernet in an enterprise environment.

If I use Bonjour service discovery, is this what I will be getting?

Probably. Network framework generally tries to connect over all possible paths — see Happy Eyeballs — and then uses the fastest one. However, the actual details are complex.

What does Network do if the LAN network does not perform well?

In that case it’s possible that peer-to-peer Wi-Fi might win the race, in which case the connection will run over that.

Will it swap the underlying connection if it figures out there is something better?

Bah, that also depends. For TCP and UDP there’s no way to swap the underlying connection once the connection has been established. However, you can install a betterPathUpdateHandler callback and then perform your own migration.

For MPTCP and QUIC it’s theoretically possible for the transport to switch for you. I don’t have enough experience with either of those cases to offer any insight into the practicality of this.


To get started, check out the TicTacToe Building a custom peer-to-peer protocol sample code.

I also recommend you have a read of:

For advice beyond that, it’d be good if you posted more info about your requirements. Things like:

  • How many messages do you expect to send per second?

  • And what’s a typical size?

  • What proportion of them must delivered reliably?

  • What proportion of them are latency sensitive?

  • And roughly what sort of latency are we talking about?

That’ll help guide your transport protocol choice, which in turn which help guide your API choice and approach.

Share and Enjoy

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

I can start by answering your questions at the end, but I suspect the best response you can provide to them will be "yeah that's rough I guess there isn't much that can be done here". But as I implement this I'm actually more interested in some of the other stuff right now–which I describe below–so I don't mind it if we don't have anything actionable here yet.

How many messages do you expect to send per second?

Several hundred.

And what’s a typical size?

Somewhat bimodal. I am working on a video streaming application, so "control" messages range from tens of bytes to say a few kilobytes. And then there are video frames, which are much larger–hundreds of kilobytes to a few megabytes (at least I think this is about right–I'm still working on optimizing this).

What proportion of them must delivered reliably?

Control messages must be delivered reliably and in order. Currently I send video frames on the same stream which makes things easier for me, but in theory I guess you can drop and reorder these and I can make a best-effort rendition on the other end based on what I get.

What proportion of them are latency sensitive?

I know this isn't very helpful, but "all of them". Let's say control messages are a little more important just because they include metadata of what is going on, but in general I want all the messages as fast as possible.

And roughly what sort of latency are we talking about?

"As little as possible". In practice let's say 10-50ms would be pretty good.

Currently I send everything over TCP, which works but isn't great. With my current understanding I think what I "should" be doing is running my control messages over TCP and making a separate UDP connection for video frames (you can make two connections to the same endpoint, right?). I don't know much about any other transport formats but if you have suggestions here I'm all ears.

With that out of the way, I hear you about the underlying protocol. I am actually very open to not worrying about it and having the system pick whatever is the ideal path (in this case, whatever is fastest/has the least latency). From your responses it seems like the way I should do that is to basically allow the system the most flexibility to pick a path (including using includePeerToPeer) and then responding to path updates as network conditions change.

I've actually already done the first part, and it looks pretty similar to the TicTacToe example you linked. The API I expose to my app is a bidirectional "pipe" you can send data on, using the Network framework. This works, but now I need to think about the second part and I'm a little confused about migrating the connection.

At a high level, there are several things I need to do: I need to somehow create a new connection, then gracefully migrate to using that, then dispose of the original one. Each part poses challenges I have questions about.

When making a new connection, what I really want to do is make a connection to the same endpoint–which is terminology I use abstractly right now, but I want to actually join this up to the classes in Network.framework. In my own words I want a new path to the same endpoint which means I create a new connection. Is this literally what I will do using the APIs? Namely, can I use the same NWEndpoint (in fact can I just yank it out of the existing NWConnection?) or do I need to do service discovery again? I assume I need to create a new NWConnection with this NWEndpoint (either the same one if this is allowed, or a new one that I re-discovered?) Will this automatically choose a new NWPath and know which one is the best? If code is clearer than my words, my question is mostly whether reconnection in this case means something like this:

let originalConnection = /* whatever */
print(originalConnection.currentPath) // some bad path
/* betterPathUpdateHandler gets called */
let newConnection = NWConnection(to: originalConnection.endpoint, using: originalConnection.parameters) // Is this the right way to make a new connection?
print(newConnection.currentPath) // will this print a better path now?

Once I've made a new connection, I need to migrate traffic over to it. If I'm using UDP and my clients above me know this then things seem easy, I can just swap the old and new connection and whatever is in flight gets dropped. However if I am promising sequential delivery then it seems like I need to do more work here. The first thing I think I want to do is to send NWConnection.ContentContext.finalMessage from the "host" (assuming this is the side that got the path update handler called first) to close the connection to new data. Once this happens I assume the host cannot send new data. However, the other side ("endpoint") won't know that I did this until it receives the finalMessage. Is it legal for the endpoint to keep sending data before this finalMessage gets to it? Can it keep sending data from its side until it sends its own finalMessage back to the host? I'm trying to understand whether this finalMessage is meant to unidirectional or whether it applies to data flowing on both sides of the connection.

Once I've finally stopped traffic on the old connection I can finally swap in the updated one for sending new traffic. To clean up the old one, I assume I should call cancel() on the NWConnection? Or should I be using cancelCurrentEndpoint() (I'm not entirely sure what this does)?

If you have a sample project that actually implements betterPathUpdateHandler I think that would clear up a lot of these questions :)

Currently I send everything over TCP, which works but isn't great.

Right. There are a couple of problems using TCP for this:

  • You have to implement your own framing protocol over TCP. If you do that in the most obvious way, a large message, like a video frame, can unnecessarily delay a small message, like a control commmand.

  • TCP won’t deliver byte N before it’s delivered bytes 0..<N. So, if the network drops a packet then every byte in the TCP byte stream after that has to wait until that packet is retransmitted. This isn’t a problem if you’re working with control messages, but it is a problem with video frame messages because, by the time the retransmit occurs, the video frame will likely to be too late to be useful.

  • Additionally, the act of retransmitting the packet containing the video frame clogs up the network unnecessarily, which may cause the problems to compound.

The fix is to use a reliable protocol for your control messages and best-effort protocol for your video frames. Traditionally that was done by using TCP for the first and UDP for the second. These days you can use QUIC for both.

you can make two connections to the same endpoint, right?

Yes, but no also no. Bonjour endpoints encode the service type, which in turn encodes the protocol. That’s what the _tcp is in _http._tcp. Advertising a TCP service over Bonjour and then using UDP to connect to a parallel UDP service might actually work, but it’s certainly weird.

To make this work you’ll have to ensure that both your TCP and UDP listeners use the same port number.

One way to avoid this rigamarole is to use QUIC, but that requires QUIC datagram support which was added in iOS 16 and friends. Is that acceptable?

"As little as possible". In practice let's say 10-50ms would be pretty good.

It’s going to be very hard to meet that in the general case. Specifically, peer-to-peer Wi-Fi often introduces significant latency to traffic — hundreds of milliseconds — and that’s not just limited to peer-to-peer traffic but it also affects infrastructure traffic. That’s why peer-to-peer is disabled by default.

I discuss the whole latency thing in a lot of detail in Investigating Network Latency Problems.

At this point I’m going to recommend that you create a small test project that streams data from one device to another using UDP. You can use that to explore the latency environment, and especially the impact of peer-to-peer on that.

Share and Enjoy

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

One way to avoid this rigamarole is to use QUIC, but that requires QUIC datagram support which was added in iOS 16 and friends. Is that acceptable?

So, in this case I can actually support the latest OSes and up–QUIC sounds like something I should definitely look into. I don't know too much about it, but my understanding is it requires TLSv1.3 for all communications. I'm currently using TLSv1.2-PSK for authentication (not 1.3 because of https://developer.apple.com/forums/thread/688508). Do you have any suggestions here before I go figure out how to generate client certificates on iOS?

Yes, but no also no. Bonjour endpoints encode the service type, which in turn encodes the protocol. That’s what the _tcp is in _http._tcp. Advertising a TCP service over Bonjour and then using UDP to connect to a parallel UDP service might actually work, but it’s certainly weird.

Ok so to be clear I suggested making multiple connections for two reasons. One was "I want a TCP stream a UDP stream" which you answered but the other reason I wanted to make multiple connections was that if I was doing a migration to a better path I would expect that I would make a second connection (of the same type) and then keep both around until the first one had quiesced and the second was ready to take over. I did actually write some code to do what I described above and it seems to not have gone very well–I got logs about the interface being in use. So I assume that I've either done something wrong here or the answer to my second part is also "don't do that". In either case I am still not entirely sure how to do a path migration.

At this point I’m going to recommend that you create a small test project that streams data from one device to another using UDP.

I actually have a library where all this code lives and it has tests in it, which is what I've been using to try this stuff out. But I'm not a network expert so it's hard to tell whether the numbers are getting are what I should expect or whether I did something horribly wrong :P

Do you have any suggestions here before I go figure out how to generate client certificates on iOS?

No. But, as mentioned in that other thread, an ER for a TLS PSK that’s compatible with QUIC would be most welcome.

Regarding TLS PKI, you’ll need to generate digital identities for both the client and the server. The iOS SDK has no API for this, so I generally point folks to Swift Certificates.

Ok so to be clear I suggested making multiple connections for two reasons.

Right. I’ve never tried to do an explicit migration from peer-to-peer Wi-Fi to infrastructure Wi-Fi, or vice versa, so I’ve no direct experience as to how feasible that is.

But I'm not a network expert so it's hard to tell whether the numbers are getting are what I should expect or whether I did something horribly wrong :P

Well, I generally start with Ethernet, because Wi-Fi has so many traps.

Share and Enjoy

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

P2P networking between Apple devices
 
 
Q