From 7dc3b2e33aea3aa29984e8febc15caf752a4dfd6 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Tue, 22 Sep 2026 09:45:13 -0400 Subject: [PATCH 1/2] fix(quic): reuse dual-stack wildcard listener --- libp2p/transports/quictransport.nim | 12 ++++++++++++ tests/libp2p/transports/test_quic.nim | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/libp2p/transports/quictransport.nim b/libp2p/transports/quictransport.nim index d48711ffe0..2838c7d117 100644 --- a/libp2p/transports/quictransport.nim +++ b/libp2p/transports/quictransport.nim @@ -613,6 +613,18 @@ proc listenerEndpointFor( return ok(Opt.none(QuicEndpoint)) matchedEndpoint = Opt.some(endpoint) + if matchedEndpoint.isSome() or address.family != AddressFamily.IPv4: + return ok(matchedEndpoint) + + when defined(linux): + for endpoint in self.listeners: + let local = endpoint.localAddress() + if local.family == AddressFamily.IPv6 and local.isAnyLocal() and + endpoint.datagramTransport().fd.getDualstack().get(false): + if matchedEndpoint.isSome(): + return ok(Opt.none(QuicEndpoint)) + matchedEndpoint = Opt.some(endpoint) + ok(matchedEndpoint) proc newDialEndpoint( diff --git a/tests/libp2p/transports/test_quic.nim b/tests/libp2p/transports/test_quic.nim index c3aa76827f..349549c9f1 100644 --- a/tests/libp2p/transports/test_quic.nim +++ b/tests/libp2p/transports/test_quic.nim @@ -322,6 +322,27 @@ suite "Quic transport": # same port as the IPv6 listener means that listener was reused extractPort(serverConn.observedAddr.get()) == dialerIPv6Port + asyncTest "IPv4 dial reuses an IPv6 wildcard listener": + let dialer = await createQuicTransport( + isServer = true, addresses = @[ma("/ip6/::/udp/0/quic-v1")] + ) + let server = + await createQuicTransport(isServer = true, addresses = @[QuicAutoAddressIP4]) + defer: + await allFutures(dialer.stop(), server.stop()) + + let dialerPort = extractPort(dialer.addrs[0]) + + let acceptFut = server.accept() + let dialerConn = await dialer.dial("", server.addrs[0]) + let serverConn = await acceptFut + defer: + await allFutures(dialerConn.close(), serverConn.close()) + + check: + serverConn.observedAddr.isSome() + extractPort(serverConn.observedAddr.get()) == dialerPort + asyncTest "dial uses an IPv6 dial-only endpoint when only an IPv4 listener exists": # An IPv4 socket cannot carry an IPv6 dial, so the IPv4 listener cannot be # reused and a separate IPv6 dial-only endpoint has to be opened. From 8e5dd920c2d8e6ddbd82c7a53588508c39b44a95 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Tue, 22 Sep 2026 10:56:39 -0400 Subject: [PATCH 2/2] fix(quic): reuse dual-stack listeners cross-platform --- libp2p/transports/quictransport.nim | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/libp2p/transports/quictransport.nim b/libp2p/transports/quictransport.nim index 2838c7d117..bc642608c8 100644 --- a/libp2p/transports/quictransport.nim +++ b/libp2p/transports/quictransport.nim @@ -616,14 +616,13 @@ proc listenerEndpointFor( if matchedEndpoint.isSome() or address.family != AddressFamily.IPv4: return ok(matchedEndpoint) - when defined(linux): - for endpoint in self.listeners: - let local = endpoint.localAddress() - if local.family == AddressFamily.IPv6 and local.isAnyLocal() and - endpoint.datagramTransport().fd.getDualstack().get(false): - if matchedEndpoint.isSome(): - return ok(Opt.none(QuicEndpoint)) - matchedEndpoint = Opt.some(endpoint) + for endpoint in self.listeners: + let local = endpoint.localAddress() + if local.family == AddressFamily.IPv6 and local.isAnyLocal() and + endpoint.datagramTransport().fd.getDualstack().get(false): + if matchedEndpoint.isSome(): + return ok(Opt.none(QuicEndpoint)) + matchedEndpoint = Opt.some(endpoint) ok(matchedEndpoint)