Skip to content

Commit 3cf9760

Browse files
committed
discv5: hole punching text updates
1 parent 1605ad0 commit 3cf9760

File tree

1 file changed

+95
-68
lines changed

1 file changed

+95
-68
lines changed

discv5/discv5-theory.md

Lines changed: 95 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -334,70 +334,96 @@ the distance to retrieve more nodes from adjacent k-buckets on `B`:
334334
Node `A` now sorts all received nodes by distance to the lookup target and proceeds by
335335
repeating the lookup procedure on another, closer node.
336336

337-
## Hole punch asymmetric NATs
337+
## Hole-punching Asymmetric NATs
338+
339+
This section explains the hole punching mechanism built into the protocol, which is
340+
enabled by the [RELAYINIT] and [RELAYMSG] message types. Compared to other protocol
341+
messages, these require deeper interaction with the session layer in order to ensure the
342+
hole punching mechanism operates safely.
343+
344+
In the examples below, we assume that node `A` (Alice) has the goal of sending a request
345+
message (e.g. FINDNODE) to node `B` (Bob).
346+
347+
Bob operates behind a network-adress-translation (NAT) layer, and is unable to receive UDP
348+
packets from Alice initially. However, Bob has previously communicated with a third node
349+
`R` (Relay), and is able to receive incoming packets from the Relay node. We further
350+
assume Bob's NAT is 'asymmetric', i.e. the IP/Port of Bob's packets will be the same
351+
regardless of the host they are sent to. The hole-punching mechanism does not work for
352+
'symmetric' NAT where every destination host has a unique mapping.
353+
354+
Node Alice may or may not behind a symmetric NAT.
355+
356+
Finally, it is assumed that a common lower bound on lifetime of NAT mappings is 20
357+
seconds, and that mappings will be refreshed when any packet is sent through them. For
358+
more background information about common NAT setups, please consult [RFC4787], [RFC6146]
359+
and [this paper][natpaper].
338360

339361
### Message flow
340362

341-
There are 4 total message containers, these are abbreviated in the sequence diagram below
342-
as follows:
363+
In the wire protocol, there are four packet types. Since the NAT-related messages require
364+
deeper integration with the packet/session layer, the packet type is explicitly shown for
365+
each message in the diagram below. We use these abbreviations:
343366

344-
- m - [message packet]
345367
- whoareyou - [WHOAREYOU packet]
346-
- hm - [handshake message packet]
347-
- s - [session message packet]
368+
- `m(X)`: [message packet] containing request `X`
369+
- `H(X)`: [handshake message packet] containing request `X`
370+
- `s(M)`: [session message packet] containing message `M`
348371

349372
```mermaid
350373
sequenceDiagram
351374
participant Alice
352375
participant Relay
353376
participant Bob
354377
355-
Relay-->>Alice: m(NODES[Bob's ENR])
378+
Relay-->>Alice: s(NODES[Bob's ENR])
356379
Alice->>Bob: m(nonce,FINDNODE)
357380
Note left of Alice:Hole punched in Alice's NAT for Bob
358381
Note left of Alice:FINDNODE timed out
359382
Alice->>Relay: s(RELAYINIT[nonce])
360383
Relay->>Bob: s(RELAYMSG[nonce])
361384
Bob-->>Alice: whoareyou(nonce)
362385
Note right of Bob: Hole punched in Bob's NAT for Alice
363-
Alice-->>Bob: hm(FINDNODE)
386+
Alice-->>Bob: H(FINDNODE)
364387
```
365388
366-
Bob is behind NAT. Bob is in Relay's kbuckets, they have a session together and Bob has
367-
sent a packet to Relay in the last ~20 seconds hence Relay can get through Bob's NAT[^1].
389+
Preconditions: Bob is behind NAT. Bob is contained in Relay's node table, they have an
390+
established session and Bob has sent a packet to Relay in the last ~20 seconds hence Relay
391+
can get through Bob's NAT.
368392
369393
As part of recursive query for peers, Alice sends a [FINDNODE] request to Bob, who's ENR
370-
it received from Relay. By making an outgoing request to Bob, if Alice is behind NAT,
371-
Alice's NAT adds the filtering rule `(Alice's-LAN-ip, Alice's-LAN-port, Bob's-WAN-ip,
372-
Bob's-WAN-port, entry-lifetime)` to it's UDP session table[^2] [^3]. This means a hole now
373-
is punched for Bob in Alice's NAT for the duration of `entry-lifetime`. The request to Bob
374-
times out as Bob is behind NAT.
375-
376-
Alice initiates an attempt to punch a hole in Bob's NAT via Relay. Alice resets the
377-
request time-out on the timed out [FINDNODE] message and wraps the message's nonce in a
378-
[RELAYINIT] notification and sends it to Relay. The notification also contains its ENR and
379-
Bob's node id.
380-
381-
Relay disassembles the [RELAYINIT] notification and uses the `target-id` to look up Bob's
382-
ENR in its kbuckets. With high probability, Relay will find Bob's ENR in its kbuckets as
383-
~1 second ago, Relay assembled a [NODES] response for Alice containing Bob's ENR (see [UDP
384-
Communication] for recommended time-out duration). Relay assembles a [RELAYMSG]
385-
notification with Alice's message nonce and ENR, then sends it to the address in Bob's
386-
ENR.
394+
it just received from the Relay. By making an outgoing request to Bob, if Alice is behind
395+
NAT, Alice's NAT adds a mapping `(Alice's-LAN-ip, Alice's-LAN-port, Bob's-WAN-ip,
396+
Bob's-WAN-port, entry-lifetime)`. This means a hole now is punched for Bob in Alice's NAT
397+
for the duration of `entry-lifetime`. However, Alice's request is not delivered as Bob is
398+
behind NAT.
399+
400+
Alice detects the timeout, and initiates an attempt to punch a hole in Bob's NAT via
401+
Relay. Alice resets the request time-out on the timed out [FINDNODE] message and wraps the
402+
message's nonce in a [RELAYINIT] notification and sends it to Relay. The notification also
403+
contains its ENR and Bob's node ID.
404+
405+
The Relay node validates the [RELAYINIT] notification and uses the `target-id` to look up
406+
Bob's ENR in its node table. Bob is very likely to be a member of the Relay's table
407+
because it was just sent to Alice in a [NODES] response. Note that, if Bob is not
408+
contained in the table, communication ends here.
409+
410+
The Relay sends a [RELAYMSG] notification containing Alice's message nonce and ENR to Bob.
387411
388412
Bob disassembles the [RELAYMSG] and uses the `nonce` to assemble a [WHOAREYOU packet],
389-
then sends it to Alice using the address in the `initiator-enr`. Bob's NAT adds the
390-
filtering rule `(Bob's-LAN-ip, Bob's-LAN-port, Alice's-WAN-ip, Alice's-WAN-port,
391-
entry-lifetime)` to it's UDP session table[^2] [^3]. A hole is punched in Bob's NAT for
392-
Alice for the duration of `entry-lifetime`.
413+
then sends it to Alice. Bob knows about Alice's endpoint from the `initiator-enr` given in
414+
in RELAYMSG.
415+
416+
Bob's NAT adds the mapping `(Bob's-LAN-ip, Bob's-LAN-port, Alice's-WAN-ip,
417+
Alice's-WAN-port, entry-lifetime)`. A hole is punched in Bob's NAT for Alice for the
418+
duration of `entry-lifetime`.
393419
394420
From here on it's business as usual. See [Sessions].
395421
396422
### Redundancy of ENRs in NODES responses and connectivity status assumptions about Relay and Bob
397423
398424
Often the same peers get passed around in NODES responses by different peers. The chance
399425
of seeing a peer received in a NODES response again in another NODES response is high as
400-
kbuckets favour long lived connections to new ones[^4]. This makes the need for a storing
426+
k-buckets favour long lived connections to new ones. This makes the need for a storing
401427
back up relays for peers small.
402428
403429
Apart from the state that is saved by not storing more than the last peer to send us an
@@ -408,51 +434,54 @@ hence of its ability to relay.
408434
### Job of keeping the hole punched falls on Bob and Bob's incentive to do so
409435
410436
UDP session table entry lifetimes are configurable, though a common lower bound is 20
411-
seconds[^1]. Bob must periodically reset the session table entry for Alice in its NAT to
412-
keep the hole for Alice punched. If Alice too is behind NAT, it must do the same for Bob.
437+
seconds. Bob must periodically reset the session table entry for Alice in its NAT to keep
438+
the hole for Alice punched. If Alice too is behind NAT, it must do the same for Bob.
413439
Implementations must ensure, when a node behind NAT does not send a packet to a peer
414440
within the entry lifetime then an empty packet is sent that is dropped by the peer.
415441
416442
NAT hole punching unavoidably creates overhead for all nodes in the network but once a
417443
hole is punched, keeping it punched requires no interaction between peers. The incentive
418444
for nodes behind NAT to keep holes for its peers punched is to avoid reestablishing a
419-
session. If there is no hole for Alice in Bob's NAT when Alice carries out a
420-
[liveness check], Bob is considered offline and the session to Bob useless. If the node
421-
behind NAT intends to frequently communicate with a peer, reestablishing the session is
422-
more costly than managing the interval of sent packets to that peer.
445+
session. If there is no hole for Alice in Bob's NAT when Alice carries out a [liveness
446+
check], Bob is considered offline and the session to Bob useless. If the node behind NAT
447+
intends to frequently communicate with a peer, reestablishing the session is more costly
448+
than managing the interval of sent packets to that peer.
423449
424450
### Discovering if the local node is behind NAT and must keep holes for peers punched
425451
426-
A node may at start-up be assigned an externally reachable socket to advertise as well as a
427-
listen socket. If those sockets are not equivalent, the node is behind NAT and the
452+
A node may at start-up be assigned an externally reachable socket to advertise as well as
453+
a listen socket. If those sockets are not equivalent, the node is behind NAT and the
428454
[mechanism for keeping holes punched] is activated. Like so, a node assumes it is behind
429455
NAT if an externally reachable socket is omitted from the initial configuration and must
430-
activate the [mechanism for keeping holes punched]. [Runtime address discovery] asses the
431-
external socket used by the local node. Once a new externally reachable socket is known,
432-
implementations will try to bind to its IP address at some number of randomly selected ports
433-
from a given range of probably unused ports. If binding succeeds with any port, the node is
434-
not behind NAT and the [mechanism for keeping holes punched] is deactivated. This solution
435-
assumes, in most scenarios where port-forwarding cannot be configured the local node host's
436-
address is private to the address realm of the device operating the NAT level furthest from
437-
the local node host[^1]. If the host and NAT device use the same IP address, binding will
438-
always succeed, so this method may give a false negative. However, this is not detrimental.
439-
A node behind NAT that deactivates the [mechanism for keeping holes punched] will more
440-
frequently have to re-establish sessions to its peers.
456+
activate the mechanism for keeping holes punched. The [runtime address discovery]
457+
mechanism can discovery the external endpoint address used by the local node. Once a new
458+
externally reachable endpoint is known, implementations will try to bind to its IP address
459+
at some number of randomly selected ports from a given range of probably unused ports. If
460+
binding succeeds with any port, the node is not behind NAT and the mechanism for keeping
461+
holes punched is deactivated.
462+
463+
This solution assumes, in most scenarios where port-forwarding cannot be configured the
464+
local node host's address is private to the address realm of the device operating the NAT
465+
level furthest from the local node host. If the host and NAT device use the same IP
466+
address, binding will always succeed, so this method may give a false negative. However,
467+
this is not detrimental. A node behind NAT that deactivates the mechanism for keeping
468+
holes punched will more frequently have to re-establish sessions to its peers.
441469
442470
### Limiting resource consumption of peers behind symmetric NATs, useful for light-clients
443471
444-
Peers with unreachable ENRs do not get inserted into kbuckets. Nodes that are behind
445-
symmetric NATs will naturally never succeed in pinpointing one external socket for peers
446-
to reach them on by [runtime address discovery] and therefore their unreachable ENRs
447-
will never update to reachable ENRs. This means, these peers will never respond to
448-
requests, as only peers in kbucktes are sent requests. This does not cohere with the
449-
p2p-model, rather the server-client model where the peer with a unreachable ENR acts
450-
as the client. This misalignment is especially bothersome for well behaving (externally
451-
reachable) light-clients operating on limited resources. Discv5.2 corrects this
452-
side-effect of [runtime address discovery] by setting introducing a configurable limit to
453-
the number of sessions at a time with peers with unreachable ENRs, the lower limit
454-
being 1. Nodes must accept sessions with at least one peer with a unreachable ENR to
455-
for [runtime address discovery] to be enabled on the discv5.2 network.
472+
Peers with unreachable ENRs do not get inserted into node table buckets. Nodes that are
473+
behind symmetric NATs will naturally never succeed in pinpointing one external socket for
474+
peers to reach them on by [runtime address discovery] and therefore their unreachable ENRs
475+
will never update to reachable ENRs.
476+
477+
This means, these peers will never respond to requests, as only peers in the node table
478+
are sent requests. This does not cohere with the p2p-model, rather the server-client model
479+
where the peer with a unreachable ENR acts as the client. This misalignment is especially
480+
bothersome for well behaving (externally reachable) light-clients operating on limited
481+
resources. Discv5.2 corrects this side-effect of runtime address discovery by introducing
482+
a configurable limit to the number of sessions at a time with peers with unreachable ENRs,
483+
the lower limit being 1. Nodes must accept sessions with at least one peer with a
484+
unreachable ENR to for runtime address discovery to be enabled on the discv5.2 network.
456485
457486
### Fault tolerance
458487
@@ -475,11 +504,9 @@ number of retries) that peer is considered unresponsive and the session can be f
475504
[UDP communication]: ./discv5-wire.md#udp-communication
476505
[Sessions]: ./discv5-theory.md#sessions
477506
[liveness check]: ./discv5-theory.md#table-maintenance-in-practice
478-
[Runtime address discovery]: ./discv5-theory.md#maintaining-the-local-node-record
479507
[runtime address discovery]: ./discv5-theory.md#maintaining-the-local-node-record
480508
[mechanism for keeping holes punched]: ./discv5-theory.md#job-of-keeping-the-hole-punched-falls-on-bob-and-bob's-incentive-to-do-so
481509
482-
[^1]: https://pdos.csail.mit.edu/papers/p2pnat.pdf
483-
[^2]: https://datatracker.ietf.org/doc/html/rfc4787
484-
[^3]: https://www.ietf.org/rfc/rfc6146.txt
485-
[^4]: https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf
510+
[natpaper]: https://pdos.csail.mit.edu/papers/p2pnat.pdf
511+
[RFC4787]: https://datatracker.ietf.org/doc/html/rfc4787
512+
[RFC6146]: https://datatracker.ietf.org/doc/html/rfc6146

0 commit comments

Comments
 (0)