Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

relay/DCUtR: Add Direct Connection Upgrade through Relay protocol #173

Merged
merged 29 commits into from
Aug 23, 2021
Merged
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
727f8b1
initial DCUtR draft
vyzo May 29, 2019
9db77f0
add paragraph about stream migration
vyzo May 29, 2019
fee2b99
add boilerplate
vyzo May 29, 2019
97e5d61
fix formatting.
raulk May 29, 2019
75ed30b
Merge branch 'libp2p/master' into rfc/dcutr
mxinden Aug 11, 2021
4ccccf5
relay/DCUtR: Copy Protocol Buffer schema from Golang impl
mxinden Aug 11, 2021
4b9549a
relay/DCUtR: Add table of contents
mxinden Aug 11, 2021
73064f9
relay/DCUtR: Add note on protocol id
mxinden Aug 11, 2021
dfc988c
relay/DCUtR: Remove go specific implementation considerations
mxinden Aug 11, 2021
46bd410
relay/DCUtR: Add TODO for retry logic
mxinden Aug 11, 2021
4e94481
relay/DCUtR: Document message length prefixing
mxinden Aug 13, 2021
9d42524
relay/DCUtR: Document maximum message size
mxinden Aug 13, 2021
9958df2
relay/DCUtR: Wrap at 80 chars
mxinden Aug 17, 2021
4b7c1ce
relay/DCUtR: Add mxinden to interest group
mxinden Aug 17, 2021
fe64a21
relay/DCUtR: Document retry logic
mxinden Aug 17, 2021
db9475e
relay/DCUtR: Remove implementation specific event emission
mxinden Aug 17, 2021
2d8b38f
relay/DCUtR: Remove note on obs address sending
mxinden Aug 17, 2021
6530d45
relay/DCUtR: Update date
mxinden Aug 17, 2021
0076c69
relay/DCUtR: Add Marten to interest group
mxinden Aug 22, 2021
b420064
relay/DCUtR: Assign roles and describe hole punching on QUIC (#361)
marten-seemann Aug 23, 2021
af0b9bb
relay/DCUtR: Stress that one should connect to all addresses
mxinden Aug 23, 2021
6f475de
relay/DCUtR: Fix typo
mxinden Aug 23, 2021
17f6275
relay/DCUtR: Mention addressing specification for ObsAddrs field
mxinden Aug 23, 2021
5943d3b
relay/DCUtR: Do not reuse same stream on retry
mxinden Aug 23, 2021
6f558f1
relay/DCUtR: Reword steps for each address
mxinden Aug 23, 2021
f7b43df
relay/DCUtR: Detail on success case
mxinden Aug 23, 2021
85f567d
relay/DCUtR: Inline `Sync` reasoning
mxinden Aug 23, 2021
cab60cc
relay/DCUtR: Remove concrete success rates
mxinden Aug 23, 2021
8001cd9
relay/DCUtR: Reword
mxinden Aug 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions relay/DCUtR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Direct Connection Upgrade through Relay

| Lifecycle Stage | Maturity | Status | Latest Revision |
|-----------------|---------------|--------|--------------------|
| 1A | Working Draft | Active | r0, 2021-08-17 |

Authors: [@vyzo]

Interest Group: [@raulk], [@stebalien], [@whyrusleeping], [@mxinden], [@marten-seemann]

[@vyzo]: https://github.com/vyzo
[@raulk]: https://github.com/raulk
[@stebalien]: https://github.com/stebalien
[@whyrusleeping]: https://github.com/whyrusleeping
[@mxinden]: https://github.com/mxinden
mxinden marked this conversation as resolved.
Show resolved Hide resolved
[@marten-seemann]: https://github.com/marten-seemann

See the [lifecycle document](https://github.com/libp2p/specs/blob/master/00-framework-01-spec-lifecycle.md)
for context about maturity level and spec status.

## Table of Contents

- [Direct Connection Upgrade through Relay](#direct-connection-upgrade-through-relay)
- [Table of Contents](#table-of-contents)
- [Introduction](#introduction)
- [The Protocol](#the-protocol)
- [RPC messages](#rpc-messages)
- [FAQ](#faq)
- [References](#references)

## Introduction

NAT traversal is a quintessential problem in peer-to-peer networks.

We currently utilize relays, which allow us to traverse NATs by using
a third party as proxy. Relays are a reliable fallback, that can
connect peers behind NAT albeit with a high-latency, low-bandwidth
connection. Unfortunately, they are expensive to scale and maintain
if they have to carry all the NATed node traffic in the network.

It is often possible for two peers behind NAT to communicate directly by
utilizing a technique called _hole punching_[1]. The technique relies on the two
peers synchronizing and simultaneously opening connections to each other to
their predicted external address. It works well for UDP, and reasonably well for
TCP.

The problem in hole punching, apart from not working all the time, is
the need for rendezvous and synchronization. This is usually
accomplished using dedicated signaling servers [2]. However, this
introduces yet another piece of infrastructure, while still requiring
the use of relays as a fallback for the cases where a direct
connection is not possible.

In this specification, we describe a synchronization protocol for direct
connectivity with hole punching that eschews signaling servers and utilizes
existing relay connections instead. That is, peers start with a relay connection
and synchronize directly, without the use of a signaling server. If the hole
punching attempt is successful, the peers _upgrade_ their connection to a direct
connection and they can close the relay connection. If the hole punching attempt
fails, they can keep using the relay connection as they were.

## The Protocol

Consider two peers, `A` and `B`. `A` wants to connect to `B`, which is
behind a NAT and advertises relay addresses. `A` may itself be behind
a NAT or be a public node.

The protocol starts with the completion of a relay connection from `A`
to `B`. Upon observing the new connection, the inbound peer (here `B`)
raulk marked this conversation as resolved.
Show resolved Hide resolved
checks the addresses advertised by `A` via identify. If that set
includes public addresses, then `A` _may_ be reachable by a direct
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it possible that A may also be directly reachable at a private address if A and B are on the same local network?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is possible, but that would have been dialed directly as the private addresses are still advertised with relay addresses.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @albrow has a point. @vyzo: while that should be the case, if we want to be resilient and robust, this protocol should not make assumptions about how any other part of the system behaves. Usually those implicit assumptions make systems brittle.

Luckily our spec lifecycle process allows us to add this topic as an active discussion:

To facilitate open progress tracking and observability, as the Working Draft
evolves, the author(s) SHOULD assemble a checklist of items that are pending
specification, explicitly stating which items are compulsory for promoting the
spec to a Candidate Recommendation.

from: https://github.com/libp2p/specs/blob/master/00-framework-01-spec-lifecycle.md

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not making this assumption will make us dial private addresses in vain multiple times.
We already have a problem with that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At best, we can consider dialing them in the bidirectional part of the protocol.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if A is public and B is private, we can't possibly be behind the same NAT.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Furthermore, for the bidirectional part of the protocol we could check the public address of the other node. If that doesn't match our own, we can't possibly be behind the same NAT and dialing private addrs is pointless.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to avoid dialing private addrs if we can avoid it though. Perhaps we could still exchange them, but in a separate field. Then they can be ignored unless your public address matches the other node and you infer that you're behind the same NAT. Or your implementation may be able to always ignore them, since they would have been dialed previously.

Anyway, I agree that we could punt on this for this round and discuss when we promote to candidate rec.

connection, in which case `B` attempts a unilateral connection upgrade
by initiating a direct connection to `A`.

If the unilateral connection upgrade attempt fails or if `A` is itself a NATed
peer that doesn't advertise public address, then `B` initiates the direct
connection upgrade protocol as follows:
1. `B` opens a stream to `A` using the `/libp2p/dcutr` protocol.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the protocol name /libp2p/dcutr.

2. `B` sends to `A` a `Connect` message containing its observed (and possibly
predicted) addresses from identify and starts a timer to measure RTT of the
relay connection.
3. Upon receving the `Connect`, `A` responds back with a `Connect` message
containing its observed (and possibly predicted) addresses.
4. Upon receiving the `Connect`, `B` sends a `Sync` message and starts a timer
for half the RTT measured from the time between sending the initial `Connect`
and receiving the response. The purpose of the `Sync` message and `B`'s timer
is to allow the two peers to synchronize so that they perform a simultaneous
open that allows hole punching to succeed.
5. Simultaneous Connect. The two nodes follow the steps below in parallel for
every address obtained from the `Connect` message:
- For a TCP address:
- Upon receiving the `Sync`, `A` immediately dials the address to `B`.
- Upon expiry of the timer, `B` dials the address to `A`.
- This will result in a TCP Simultaneous Connect. For the purpose of all
protocols run on top of this TCP connection, `A` is assumed to be the
client and `B` the server.
- For a QUIC address:
- Upon receiving the `Sync`, `A` immediately dials the address to `B`.
- Upon expiry of the timer, `B` starts to send UDP packets filled with
random bytes to `A`'s address. Packets should be sent repeatedly in
random intervals between 10 and 200 ms.
- This will result in a QUIC connection where `A` is the client and `B` is
the server.
Comment on lines +89 to +103
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I see, this whole mechanism would also fit nicely upgrading the relay connection to a direct WebRTC connection, if the peers would be allowed to exchange their SDP data here.
Would you be open in amending the spec?
(cc @mxinden)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good point. We had this in mind, but as you said, it isn't mentioned anywhere. Given that the protocol uses protocol buffers, we could easily extend the messages to include additional data such as SDP payloads, or derive an SDP payload based on the information exchanged through the protocol.

Unfortunately there is no uniform way of speaking WebRTC across the many libp2p libraries (yet). In addition there is no specification yet (see #220 and #159). This is not to say that the project is not interested in adding WebRTC support in the future. Quite the opposite (see https://github.com/libp2p/specs/blob/master/connections/hole-punching.md and https://github.com/libp2p/specs/blob/master/ROADMAP.md#-unprecedented-global-connectivity).

With the above in mind, I am not sure whether it makes much sense to extend this paragraph with a section on WebRTC quite yet.

@wngr what do you think?

Copy link

@wngr wngr Sep 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think DCUtR would be a great way to add support for upgrading relayed connections to a direct WebRTC connection -- this just feels like the right abstraction, and the the alternative proposals so far appear inferior. Now I acknowledge that the big downside of this approach is that this requires a valid TLS certificate for the peer offering a WS endpoint, but I think that is a pill that can be swallowed, but that's orthogonal to the relayed connection upgrade.
In other words, I think DCUtR is the right way to add support for upgrades to WebRTC (or allow exchanging arbitrary payloads here?), and I don't want to let the current opportunity window slide ;-).

(By the way, I hacked on an experimental webrtc transport for rust-libp2p which supports both browser apis (through wasm) and native; signalling is currently done via p2p-webrtc-star.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words, I think DCUtR is the right way to add support for upgrades to WebRTC (or allow exchanging arbitrary payloads here?), and I don't want to let the current opportunity window slide ;-).

👍

(By the way, I hacked on an experimental webrtc transport for rust-libp2p which supports both browser apis (through wasm) and native; signalling is currently done via p2p-webrtc-star.)

🚀 that is great to hear. Mind opening a work-in-progress pull request on rust-libp2p @wngr?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My current WIP is at https://github.com/wngr/libp2p-webrtc; however I really want to replace the WS signalling server with a libp2p relay node; this is why I started adding my own custom (behaviour, transport) tuple on top of rust-libp2p, which very much is similar to dcutr on a higher level.
What's the state of your dcutr branch? Maybe it makes more sense to prototype it ontop of that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the state of your dcutr branch? Maybe it makes more sense to prototype it ontop of that?

You could leverage both libp2p/rust-libp2p#2059 and libp2p/rust-libp2p#2076. In case my understanding of WebRTC and SDP is correct, it solely needs to exchange a payload. If so (at least for now) you could just extend the Protobuf definition of the DCUTR protocol by a single field for that payload.

Happy to talk through this in person if that is preferred. Feel free to reach out via mail @wngr.

6. Once a single connection has been established, `A` SHOULD cancel all
outstanding connection attempts. The peers should migrate to the established
connection by prioritizing over the existing relay connection. All new
streams should be opened in the direct connection, while the relay connection
should be closed after a grace period. Existing long-lived streams
will have to be recreated in the new connection once the relay connection is
closed.

On failure of all connection attempts go back to step (1). Inbound peers
(here `B`) SHOULD retry twice (thus a total of 3 attempts) before considering
the upgrade as failed.

### RPC messages

All RPC messages sent over a stream are prefixed with the message length in
bytes, encoded as an unsigned variable length integer as defined by the
[multiformats unsigned-varint spec][uvarint-spec].

Implementations SHOULD refuse encoded RPC messages (length prefix excluded)
larger than 4 KiB.

RPC messages conform to the following protobuf schema:

```proto
syntax = "proto2";

package holepunch.pb;

message HolePunch {
enum Type {
CONNECT = 100;
SYNC = 300;
}

optional Type type=1;

repeated bytes ObsAddrs = 2;
mxinden marked this conversation as resolved.
Show resolved Hide resolved
}
```

`ObsAddrs` is a list of multiaddrs encoded in the binary multiaddr
representation. See [Addressing specification] for details.

## FAQ

- *Why exchange `CONNECT` and `SYNC` messages once more on each retry?*

Doing an additional CONNECT and SYNC for each retry prevents a flawed RTT
measurement on the first attempt to distort all following retry attempts.

## References

1. Peer-to-Peer Communication Across Network Address Translators. B. Ford and P.
Srisuresh. https://pdos.csail.mit.edu/papers/p2pnat.pdf
2. Interactive Connectivity Establishment (ICE): A Protocol for Network Address
Translator (NAT) Traversal for Offer/Answer Protocols. IETF RFC 5245.
https://tools.ietf.org/html/rfc5245

[uvarint-spec]: https://github.com/multiformats/unsigned-varint
[Addressing specification]: ../addressing/README.md