diff --git a/libp2p/builders.nim b/libp2p/builders.nim index 1a9d398bb0..86ece1ff3a 100644 --- a/libp2p/builders.nim +++ b/libp2p/builders.nim @@ -243,7 +243,7 @@ proc withWsTransport*( proc withQuicTransport*(b: SwitchBuilder): SwitchBuilder = b.withTransport( proc(config: TransportConfig): Transport = - QuicTransport.new(config.upgr, config.privateKey, config.connManager) + QuicTransport.new(config.upgr, config.privateKey, config.rng, config.connManager) ) proc withMemoryTransport*(b: SwitchBuilder): SwitchBuilder = diff --git a/libp2p/crypto/rng.nim b/libp2p/crypto/rng.nim index a99b70c4d3..2e4faf2923 100644 --- a/libp2p/crypto/rng.nim +++ b/libp2p/crypto/rng.nim @@ -113,6 +113,19 @@ proc randBelow(rng: Rng, max: uint32): int = if r >= threshold: return (r mod max).int +proc rand*(rng: Rng, low, high: int): int = + ## Return a uniformly random integer in the inclusive range [`low`, `high`]. + doAssert low <= high, "random range must not be empty" + + let width = + if low >= 0 or high < 0: + uint64(high - low) + 1 + else: + uint64(-(low + 1)) + uint64(high) + 2 + doAssert width <= uint64(uint32.high), "random range is too large" + + low + rng.randBelow(width.uint32) + proc pick*[T](rng: Rng, x: openArray[T], n: int): Opt[seq[T]] = doAssert n >= 0, "n must be non-negative" if x.len == 0: diff --git a/libp2p/dialer.nim b/libp2p/dialer.nim index a9cbf2b397..db8e1abdc5 100644 --- a/libp2p/dialer.nim +++ b/libp2p/dialer.nim @@ -56,7 +56,7 @@ proc dialAndUpgrade*( let dialed = try: libp2p_total_dial_attempts.inc() - await transport.dial(hostname, addrs, peerId) + await transport.dial(hostname, addrs, peerId, dir) except CancelledError as exc: trace "Dialing canceled", description = exc.msg, peerId raise exc diff --git a/libp2p/protocols/connectivity/relay/rtransport.nim b/libp2p/protocols/connectivity/relay/rtransport.nim index ef78ee6a1f..4d7545282f 100644 --- a/libp2p/protocols/connectivity/relay/rtransport.nim +++ b/libp2p/protocols/connectivity/relay/rtransport.nim @@ -107,6 +107,7 @@ method dial*( hostname: string, ma: MultiAddress, peerId: Opt[PeerId] = Opt.none(PeerId), + dir: Direction = Direction.Out, ): Future[RawConn] {.async: (raises: [transport.TransportError, CancelledError]).} = peerId.withValue(pid): try: diff --git a/libp2p/transports/memorytransport.nim b/libp2p/transports/memorytransport.nim index 6e408bd83a..e5d10d3f2f 100644 --- a/libp2p/transports/memorytransport.nim +++ b/libp2p/transports/memorytransport.nim @@ -97,6 +97,7 @@ method dial*( hostname: string, ma: MultiAddress, peerId: Opt[PeerId] = Opt.none(PeerId), + dir: Direction = Direction.Out, ): Future[RawConn] {.async: (raises: [transport.TransportError, CancelledError]).} = try: let listener = getInstance().dial($ma) diff --git a/libp2p/transports/quictransport.nim b/libp2p/transports/quictransport.nim index de515607fe..7a5a12c14c 100644 --- a/libp2p/transports/quictransport.nim +++ b/libp2p/transports/quictransport.nim @@ -5,6 +5,7 @@ import std/[hashes, sets, sequtils] import chronos, chronicles, metrics, results import lsquic import + ../crypto/rng, ../wire, ../connmanager, ../multiaddress, @@ -25,6 +26,8 @@ export transport logScope: topics = "libp2p quictransport" +const QuicHolePunchPacketSize* = 64 + type P2PConnection = connection.Connection QuicConnection = lsquic.Connection @@ -350,11 +353,15 @@ proc new*( _: type QuicTransport, u: Upgrade, privateKey: PrivateKey, + rng: Rng, connManager: ConnManager = nil, ): QuicTransport = + doAssert not rng.isNil, "Rng is nil" + let self = QuicTransport( upgrader: QuicUpgrade(ms: u.ms, connManager: connManager.toOpt()), privateKey: privateKey, + rng: rng, certGenerator: defaultCertGenerator, ) procCall Transport(self).initialize() @@ -364,12 +371,16 @@ proc new*( _: type QuicTransport, u: Upgrade, privateKey: PrivateKey, + rng: Rng, certGenerator: CertGenerator, connManager: ConnManager = nil, ): QuicTransport = + doAssert not rng.isNil, "Rng is nil" + let self = QuicTransport( upgrader: QuicUpgrade(ms: u.ms, connManager: connManager.toOpt()), privateKey: privateKey, + rng: rng, certGenerator: certGenerator, ) procCall Transport(self).initialize() @@ -576,6 +587,7 @@ method dial*( hostname: string, address: MultiAddress, peerId: Opt[PeerId] = Opt.none(PeerId), + dir: Direction = Direction.Out, ): Future[RawConn] {.async: (raises: [transport.TransportError, CancelledError]).} = let taAddress = try: @@ -586,6 +598,23 @@ method dial*( ) try: + if dir == Direction.In: + let endpoint = self.listenerEndpointFor(taAddress) + if endpoint.isNone(): + raise newException( + QuicTransportDialError, + "error in QUIC hole punch: no unique listener for address family", + ) + + # The Sync sender is the QUIC server. Open its NAT mapping with random + # UDP packets from the listener socket and let the expected inbound + # connection complete the DCUtR attempt. + while true: + let payload = self.rng.generateBytes(QuicHolePunchPacketSize) + await endpoint.get().datagramTransport().sendTo(taAddress, payload) + let delay = self.rng.rand(10, 200) + await sleepAsync(delay.milliseconds) + let endpoint = self.dialEndpointFor(taAddress) let quicConnection = await endpoint.dial(taAddress) peerId.withValue(expectedPeerId): @@ -606,6 +635,8 @@ method dial*( ) except TransportOsError as e: raise newException(QuicTransportDialError, "error in quic dial:" & e.msg, e) + except chronos.TransportError as e: + raise newException(QuicTransportDialError, "error in quic dial: " & e.msg, e) except DialError as e: raise newException(QuicTransportDialError, "error in quic dial:" & e.msg, e) except QuicError as e: diff --git a/libp2p/transports/tcptransport.nim b/libp2p/transports/tcptransport.nim index 6e1d2e45f7..4fc49ac6eb 100644 --- a/libp2p/transports/tcptransport.nim +++ b/libp2p/transports/tcptransport.nim @@ -286,6 +286,7 @@ method dial*( hostname: string, address: MultiAddress, peerId: Opt[PeerId] = Opt.none(PeerId), + dir: Direction = Direction.Out, ): Future[RawConn] {.async: (raises: [transport.TransportError, CancelledError]).} = ## dial a peer if self.stopping: diff --git a/libp2p/transports/tortransport.nim b/libp2p/transports/tortransport.nim index bdb404567e..e8fcbcfa9f 100644 --- a/libp2p/transports/tortransport.nim +++ b/libp2p/transports/tortransport.nim @@ -221,6 +221,7 @@ method dial*( hostname: string, address: MultiAddress, peerId: Opt[PeerId] = Opt.none(PeerId), + dir: Direction = Direction.Out, ): Future[RawConn] {.async: (raises: [transport.TransportError, CancelledError]).} = ## dial a peer ## diff --git a/libp2p/transports/transport.nim b/libp2p/transports/transport.nim index ac81c58666..83f95460c8 100644 --- a/libp2p/transports/transport.nim +++ b/libp2p/transports/transport.nim @@ -75,6 +75,7 @@ method dial*( hostname: string, address: MultiAddress, peerId: Opt[PeerId] = Opt.none(PeerId), + dir: Direction = Direction.Out, ): Future[RawConn] {.base, gcsafe, async: (raises: [TransportError, CancelledError]).} = ## dial a peer ## diff --git a/libp2p/transports/wstransport.nim b/libp2p/transports/wstransport.nim index fdf92c6a0c..e6c3d83ec9 100644 --- a/libp2p/transports/wstransport.nim +++ b/libp2p/transports/wstransport.nim @@ -506,6 +506,7 @@ method dial*( hostname: string, address: MultiAddress, peerId: Opt[PeerId] = Opt.none(PeerId), + dir: Direction = Direction.Out, ): Future[RawConn] {.async: (raises: [transport.TransportError, CancelledError]).} = ## dial a peer ## diff --git a/tests/libp2p/protocols/test_dcutr.nim b/tests/libp2p/protocols/test_dcutr.nim index a7cbfd48bb..6ecf4e3c7f 100644 --- a/tests/libp2p/protocols/test_dcutr.nim +++ b/tests/libp2p/protocols/test_dcutr.nim @@ -108,7 +108,7 @@ suite "Dcutr": # inbound identify stream check await directConnIdentified.withTimeout(30.seconds) - asyncTest "DCUtR establishes a new QUIC connection": + asyncTest "DCUtR QUIC initiator uses listener role": let behindNATSwitch = makeSwitch(QuicAutoAddress) let publicSwitch = makeSwitch(QuicAutoAddress) @@ -126,35 +126,18 @@ suite "Dcutr": behindNATSwitch.connManager.connCount(publicSwitch.peerInfo.peerId) check initialConnCount == 1 - let directConnSeen = - Future[void].Raising([CancelledError]).init("dcutr direct connection seen") - - proc onDirectConn( - peerId: PeerId, event: ConnEvent - ): Future[void] {.async: (raises: [CancelledError]).} = - if peerId == publicSwitch.peerInfo.peerId and - behindNATSwitch.connManager.connCount(publicSwitch.peerInfo.peerId) > - initialConnCount: - directConnSeen.completeOnce() - - # use event handler to wait-and-assert exact moment when condition - # (publicSwitch has connected to behindNATSwitch) is satisfied - # instead of polling, as polling can miss a short-lived true state - behindNATSwitch.connManager.addConnEventHandler( - onDirectConn, ConnEventKind.Connected - ) - for t in behindNATSwitch.transports: t.networkReachability = NetworkReachability.NotReachable - await DcutrClient - .new(connectTimeout = 5.seconds) - .startSync( + # A direct QUIC connection stands in for the relay, so the listener-role + # hole punch must time out. + try: + await DcutrClient.new(connectTimeout = 300.millis).startSync( behindNATSwitch, publicSwitch.peerInfo.peerId, behindNATSwitch.peerInfo.addrs ) - .wait(10.seconds) - - check await directConnSeen.withTimeout(10.seconds) + check false + except DcutrError as err: + check err.parent of AsyncTimeoutError template ductrClientTest( behindNATSwitch: Switch, publicSwitch: Switch, body: untyped diff --git a/tests/libp2p/transports/test_quic.nim b/tests/libp2p/transports/test_quic.nim index 8c754b966f..0025acb1c7 100644 --- a/tests/libp2p/transports/test_quic.nim +++ b/tests/libp2p/transports/test_quic.nim @@ -9,6 +9,7 @@ import transports/transport, transports/quictransport, upgrademngrs/upgrade, + utils/future, muxers/muxer, multiaddress, ] @@ -19,7 +20,7 @@ import ./utils proc quicTransProvider(): Transport {.gcsafe, raises: [].} = try: - return QuicTransport.new(Upgrade(), PrivateKey.random(ECDSA, rng()).tryGet()) + return QuicTransport.new(Upgrade(), PrivateKey.random(ECDSA, rng()).tryGet(), rng()) except ResultError[crypto.CryptoError]: raiseAssert "should not happen" @@ -55,6 +56,36 @@ suite "Quic transport": streamProvider, ) + asyncTest "listener-role dial sends UDP hole-punch packets": + let packetReceived = + Future[int].Raising([CancelledError]).init("QUIC hole-punch packet received") + + proc receivePacket( + transp: DatagramTransport, remote: TransportAddress + ): Future[void] {.async: (raises: []).} = + try: + packetReceived.completeOnce(transp.getMessage().len) + except chronos.TransportError: + discard + + let receiver = + newDatagramTransport(receivePacket, local = initTAddress("127.0.0.1:0")) + defer: + receiver.close() + + let puncher = await createQuicTransport(isServer = true) + defer: + await puncher.stop() + + let remoteAddr = MultiAddress + .init("/ip4/127.0.0.1/udp/" & $receiver.localAddress().port & "/quic-v1") + .tryGet() + let punchFut = puncher.dial("", remoteAddr, Opt.none(PeerId), Direction.In) + defer: + await punchFut.cancelAndWait() + + check (await packetReceived) == QuicHolePunchPacketSize + asyncTest "transport e2e - invalid cert - server": let server = await createQuicTransport(isServer = true, withInvalidCert = true) asyncSpawn createServerAcceptConn(server)() diff --git a/tests/libp2p/transports/utils.nim b/tests/libp2p/transports/utils.nim index a6d0cdea17..4af15635df 100644 --- a/tests/libp2p/transports/utils.nim +++ b/tests/libp2p/transports/utils.nim @@ -90,9 +90,9 @@ proc createQuicTransport*( let trans = if withInvalidCert: - QuicTransport.new(Upgrade(), key, invalidCertGenerator) + QuicTransport.new(Upgrade(), key, rng(), invalidCertGenerator) else: - QuicTransport.new(Upgrade(), key) + QuicTransport.new(Upgrade(), key, rng()) if isServer: # servers are started because they need to listen await trans.start(addresses)