Skip to content

Commit 097a471

Browse files
committed
fix(dcutr): use listener role for QUIC hole punching
1 parent 13473bc commit 097a471

12 files changed

Lines changed: 79 additions & 30 deletions

File tree

libp2p/builders.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ proc withWsTransport*(
243243
proc withQuicTransport*(b: SwitchBuilder): SwitchBuilder =
244244
b.withTransport(
245245
proc(config: TransportConfig): Transport =
246-
QuicTransport.new(config.upgr, config.privateKey, config.connManager)
246+
QuicTransport.new(config.upgr, config.privateKey, config.rng, config.connManager)
247247
)
248248

249249
proc withMemoryTransport*(b: SwitchBuilder): SwitchBuilder =

libp2p/dialer.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ proc dialAndUpgrade*(
5656
let dialed =
5757
try:
5858
libp2p_total_dial_attempts.inc()
59-
await transport.dial(hostname, addrs, peerId)
59+
await transport.dial(hostname, addrs, peerId, dir)
6060
except CancelledError as exc:
6161
trace "Dialing canceled", description = exc.msg, peerId
6262
raise exc

libp2p/protocols/connectivity/relay/rtransport.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ method dial*(
107107
hostname: string,
108108
ma: MultiAddress,
109109
peerId: Opt[PeerId] = Opt.none(PeerId),
110+
dir: Direction = Direction.Out,
110111
): Future[RawConn] {.async: (raises: [transport.TransportError, CancelledError]).} =
111112
peerId.withValue(pid):
112113
try:

libp2p/transports/memorytransport.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ method dial*(
9797
hostname: string,
9898
ma: MultiAddress,
9999
peerId: Opt[PeerId] = Opt.none(PeerId),
100+
dir: Direction = Direction.Out,
100101
): Future[RawConn] {.async: (raises: [transport.TransportError, CancelledError]).} =
101102
try:
102103
let listener = getInstance().dial($ma)

libp2p/transports/quictransport.nim

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import std/[hashes, sets, sequtils]
55
import chronos, chronicles, metrics, results
66
import lsquic
77
import
8+
../crypto/rng,
89
../wire,
910
../connmanager,
1011
../multiaddress,
@@ -350,11 +351,15 @@ proc new*(
350351
_: type QuicTransport,
351352
u: Upgrade,
352353
privateKey: PrivateKey,
354+
rng: Rng,
353355
connManager: ConnManager = nil,
354356
): QuicTransport =
357+
doAssert not rng.isNil, "Rng is nil"
358+
355359
let self = QuicTransport(
356360
upgrader: QuicUpgrade(ms: u.ms, connManager: connManager.toOpt()),
357361
privateKey: privateKey,
362+
rng: rng,
358363
certGenerator: defaultCertGenerator,
359364
)
360365
procCall Transport(self).initialize()
@@ -364,12 +369,16 @@ proc new*(
364369
_: type QuicTransport,
365370
u: Upgrade,
366371
privateKey: PrivateKey,
372+
rng: Rng,
367373
certGenerator: CertGenerator,
368374
connManager: ConnManager = nil,
369375
): QuicTransport =
376+
doAssert not rng.isNil, "Rng is nil"
377+
370378
let self = QuicTransport(
371379
upgrader: QuicUpgrade(ms: u.ms, connManager: connManager.toOpt()),
372380
privateKey: privateKey,
381+
rng: rng,
373382
certGenerator: certGenerator,
374383
)
375384
procCall Transport(self).initialize()
@@ -576,6 +585,7 @@ method dial*(
576585
hostname: string,
577586
address: MultiAddress,
578587
peerId: Opt[PeerId] = Opt.none(PeerId),
588+
dir: Direction = Direction.Out,
579589
): Future[RawConn] {.async: (raises: [transport.TransportError, CancelledError]).} =
580590
let taAddress =
581591
try:
@@ -586,6 +596,23 @@ method dial*(
586596
)
587597

588598
try:
599+
if dir == Direction.In:
600+
let endpoint = self.listenerEndpointFor(taAddress)
601+
if endpoint.isNone():
602+
raise newException(
603+
QuicTransportDialError,
604+
"error in QUIC hole punch: no unique listener for address family",
605+
)
606+
607+
# The Sync sender is the QUIC server. Open its NAT mapping with random
608+
# UDP packets from the listener socket and let the expected inbound
609+
# connection complete the DCUtR attempt.
610+
while true:
611+
let payload = self.rng.generateBytes(64)
612+
await endpoint.get().datagramTransport().sendTo(taAddress, payload)
613+
let delay = 10 + int(self.rng.generate(uint32) mod 191'u32)
614+
await sleepAsync(delay.milliseconds)
615+
589616
let endpoint = self.dialEndpointFor(taAddress)
590617
let quicConnection = await endpoint.dial(taAddress)
591618
peerId.withValue(expectedPeerId):
@@ -606,6 +633,8 @@ method dial*(
606633
)
607634
except TransportOsError as e:
608635
raise newException(QuicTransportDialError, "error in quic dial:" & e.msg, e)
636+
except chronos.TransportError as e:
637+
raise newException(QuicTransportDialError, "error in quic dial: " & e.msg, e)
609638
except DialError as e:
610639
raise newException(QuicTransportDialError, "error in quic dial:" & e.msg, e)
611640
except QuicError as e:

libp2p/transports/tcptransport.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ method dial*(
286286
hostname: string,
287287
address: MultiAddress,
288288
peerId: Opt[PeerId] = Opt.none(PeerId),
289+
dir: Direction = Direction.Out,
289290
): Future[RawConn] {.async: (raises: [transport.TransportError, CancelledError]).} =
290291
## dial a peer
291292
if self.stopping:

libp2p/transports/tortransport.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ method dial*(
221221
hostname: string,
222222
address: MultiAddress,
223223
peerId: Opt[PeerId] = Opt.none(PeerId),
224+
dir: Direction = Direction.Out,
224225
): Future[RawConn] {.async: (raises: [transport.TransportError, CancelledError]).} =
225226
## dial a peer
226227
##

libp2p/transports/transport.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ method dial*(
7575
hostname: string,
7676
address: MultiAddress,
7777
peerId: Opt[PeerId] = Opt.none(PeerId),
78+
dir: Direction = Direction.Out,
7879
): Future[RawConn] {.base, gcsafe, async: (raises: [TransportError, CancelledError]).} =
7980
## dial a peer
8081
##

libp2p/transports/wstransport.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ method dial*(
506506
hostname: string,
507507
address: MultiAddress,
508508
peerId: Opt[PeerId] = Opt.none(PeerId),
509+
dir: Direction = Direction.Out,
509510
): Future[RawConn] {.async: (raises: [transport.TransportError, CancelledError]).} =
510511
## dial a peer
511512
##

tests/libp2p/protocols/test_dcutr.nim

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ suite "Dcutr":
108108
# inbound identify stream
109109
check await directConnIdentified.withTimeout(30.seconds)
110110

111-
asyncTest "DCUtR establishes a new QUIC connection":
111+
asyncTest "DCUtR QUIC initiator uses listener role":
112112
let behindNATSwitch = makeSwitch(QuicAutoAddress)
113113
let publicSwitch = makeSwitch(QuicAutoAddress)
114114

@@ -126,35 +126,18 @@ suite "Dcutr":
126126
behindNATSwitch.connManager.connCount(publicSwitch.peerInfo.peerId)
127127
check initialConnCount == 1
128128

129-
let directConnSeen =
130-
Future[void].Raising([CancelledError]).init("dcutr direct connection seen")
131-
132-
proc onDirectConn(
133-
peerId: PeerId, event: ConnEvent
134-
): Future[void] {.async: (raises: [CancelledError]).} =
135-
if peerId == publicSwitch.peerInfo.peerId and
136-
behindNATSwitch.connManager.connCount(publicSwitch.peerInfo.peerId) >
137-
initialConnCount:
138-
directConnSeen.completeOnce()
139-
140-
# use event handler to wait-and-assert exact moment when condition
141-
# (publicSwitch has connected to behindNATSwitch) is satisfied
142-
# instead of polling, as polling can miss a short-lived true state
143-
behindNATSwitch.connManager.addConnEventHandler(
144-
onDirectConn, ConnEventKind.Connected
145-
)
146-
147129
for t in behindNATSwitch.transports:
148130
t.networkReachability = NetworkReachability.NotReachable
149131

150-
await DcutrClient
151-
.new(connectTimeout = 5.seconds)
152-
.startSync(
132+
# A direct QUIC connection stands in for the relay, so the listener-role
133+
# hole punch must time out.
134+
try:
135+
await DcutrClient.new(connectTimeout = 300.millis).startSync(
153136
behindNATSwitch, publicSwitch.peerInfo.peerId, behindNATSwitch.peerInfo.addrs
154137
)
155-
.wait(10.seconds)
156-
157-
check await directConnSeen.withTimeout(10.seconds)
138+
check false
139+
except DcutrError as err:
140+
check err.parent of AsyncTimeoutError
158141

159142
template ductrClientTest(
160143
behindNATSwitch: Switch, publicSwitch: Switch, body: untyped

0 commit comments

Comments
 (0)