libp2p / specs

Technical specifications for the libp2p networking stack
https://libp2p.io
1.56k stars 273 forks source link

using QUIC to make NAT traversal and hole punching more efficient #434

Open marten-seemann opened 2 years ago

marten-seemann commented 2 years ago

Setup

Node A, located behind a NAT / firewall is connected to a relay R. Node B, located outside of A's network, wants to connect to A. It needs to connect to A through R, as a direct connection is not possible.

Current Solution

A keeps a connection open to R. When B wants to connect to A, it first establishes a connection to R, and asks to be relayed to A. It then establishes a tunneled connection to A.

This setup comes with a couple of inefficiencies:

  1. There's one encryption context (TLS / Noise) for the connection between A and R, one for the connection between B and R, and another one for the tunneled connection between A and B. This means that A and B have to double-encrypt their data, and that R has to decrypt and then re-encrypt the data.
  2. Both the direct connection and the tunnel connections are running congestion controllers. It is known that layered congestion controllers can lead to suboptimal performance.

Possible improvements

Connection between A and R

If the connection between A and R is a QUIC connection, it is not necessary to tunnel (QUIC) packets over that connection. Just keeping the connection alive between A and R (by sending a probe packet every 15s or so) is enough to keep the NAT mapping for this 4-tuple alive. R can send packets on the same 4-tuple that belong to a different QUIC connection. Connections are demultiplexed by their QUIC Connection IDs, not by their 4-tuple. We can therefore have a practically unlimited number of QUIC connections using a single NAT binding.

Specifically, this means that if we can make R receive completes QUIC packets to forward to A, R can do so without re-encrypting them.

Connection between B and R

How can we make sure that R receives QUIC packets to forward to A? There are two options:

  1. Use MASQUE's CONNECT-UDP. In a nutshell, B establishes a HTTP/3 connection to R, setting the :path to /.well-known/masque/udp/<IP of A>/<port of A>/. B then sends QUIC packets intended for A in HTTP/3 DATAGRAM frames, R unpacks these DATAGRAMs and puts the payload on the wire between R and A. CONNECT-UDP is already used for the Relay 1 <-> Relay 2 hop in Apple's Private Relay (see diagram on page 6).
  2. Use MASQUE's CONNECT-QUIC. B opens a connection to R, and negotiates a set of QUIC connection IDs that belong to the connection that will be relayed to A. It then sends raw QUIC packets, using one of those connection IDs, to R. R, recognizing the connection ID, now forwards that packet to A. All that R has to do is look at the connection ID of the packet, it doesn't need to do any cryptographic operation. Note that this document is not officially adopted by the MASQUE working group (yet), but is already used for the Device <-> Relay 1 hop in Apple's Private Relay.

Establishing a direct connection

Now that we have a relayed connection from B (playing the role of the QUIC client) via R to A (in the role of the QUIC server), we want to establish a direct connection between B and A. Ideally, we don't want to establish a new connection, but take our relayed connection (and all the associated stream state) with us.

QUIC supports connection migration, in two slightly different flavors: Server Preferred Address and Connection Migration. Connection Migration is always initiated by the QUIC client (node B), and starts with sending a probe packet to the server (node A), in order to probe if the path works. We can use this probe packet as a hole punching packet, all we need to do is coordinate the timing of the path probing, and have node A send a single UDP packet to B's public address. This is similar to what we today achieve with the DCUtR protocol, however, we can skip the step that measures the RTT, since QUIC's congestion controller already maintains an RTT estimate. And with QUIC's timestamp extension we could even get the one-way path delay/

What do we actually gain from this?

For limited relays, avoiding double encryption is only moderately interesting. Since we're only relaying a limited amount of data, the overhead imposed by double encryption is also limited. Being able to migrate a connection instead of establishing a new connection is more interesting: it might allow us to start running application protocols on the relayed connection, significantly reducing the time until we have a usable connection.

For unlimited relays, the savings are huge. Currently, unlimited relays need 1. huge amounts of bandwidths and 2. powerful CPUs. While no improvement we make can reduce the bandwidth requirements, saving the decryption - re-encryption step will bring the CPU requirements down to (close to) zero.