Repository: libp2p/go-libp2p
Pull request #2889 (2024-07-30 04:47:52)

feat: WebRTC reuse QUIC conn
This lets users listen on the same port for WebRTC and QUIC. Kubo folks asked for this as it would be very beneficial to their deployment.

One open question for me is:
- Do we need to make sure this works well with the Observed Address Manager? I think it should just work, but we should double check. Ideally we should know our public webrtc-direct address if we've discovered our public quic address when they use the same port. 

MarcoPolo: Discussed out of band about changes to make this order agnostic with the QUIC transport, since, as it is implemented in this PR, the QUIC listener starts first. Sukun and I agreed the existing logic is good enough. Which is to sort the listeners and start the QUIC listener first (if there is one). Summary of the points we discussed:

- To reuse the same port, WebRTC has to go through a QUIC transport.
- It doesn't make sense to always instantiate a QUIC transport when setting up a WebRTC listener.
  - It makes WebRTC unnecessarily depend on a QUIC transport being present.
  - Can lead to hard to debug issues. And cause surprising behavior.
- It's much easier to depend on a QUIC transport that has already been started from a prior QUIC listener.
- Sorting the listeners before starting is relatively easy and straightforward.
MarcoPolo: > We should infer the address from the quic address like we do for webtransport. It'll work seamlessly once we have the same logic as [inferWebtransportAddrsFromQuic](https://github.com/libp2p/go-libp2p/blob/master/p2p/host/basic/basic_host.go#L974)

I don't think we actually need this anymore. I've made a PR to remove that function here https://github.com/libp2p/go-libp2p/pull/2892.



MarcoPolo: > Took it for a spin in https://github.com/ipfs/kubo/pull/10463 and  fx seems to have issue with libp2pwebrtc.ListenUDPFn.

That was my mistake sorry. I think it was complaining that it couldn't create the WebRTC transport for the AutoNAT host because I didn't provide it to it (only for the basic host).

Patch:
diff --git a/config/config.go b/config/config.go
index 3743979e..4b08c076 100644
--- a/config/config.go
+++ b/config/config.go
@@ -5,6 +5,7 @@ import (
 	"crypto/rand"
 	"errors"
 	"fmt"
+	"net"
 	"time"
 
 	"github.com/libp2p/go-libp2p/core/connmgr"
@@ -35,10 +36,12 @@ import (
 	relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
 	"github.com/libp2p/go-libp2p/p2p/protocol/holepunch"
 	"github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
+	libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc"
 	"github.com/prometheus/client_golang/prometheus"
 
 	ma "github.com/multiformats/go-multiaddr"
 	madns "github.com/multiformats/go-multiaddr-dns"
+	manet "github.com/multiformats/go-multiaddr/net"
 	"github.com/quic-go/quic-go"
 	"go.uber.org/fx"
 	"go.uber.org/fx/fxevent"
@@ -284,6 +287,29 @@ func (cfg *Config) addTransports() ([]fx.Option, error) {
 		fx.Provide(func() pnet.PSK { return cfg.PSK }),
 		fx.Provide(func() network.ResourceManager { return cfg.ResourceManager }),
 		fx.Provide(func() *madns.Resolver { return cfg.MultiaddrResolver }),
+		fx.Provide(func(cm *quicreuse.ConnManager, sw *swarm.Swarm) libp2pwebrtc.ListenUDPFn {
+			hasQuicAddrPortFor := func(network string, laddr *net.UDPAddr) bool {
+				quicAddrPorts := map[string]struct{}{}
+				for _, addr := range sw.ListenAddresses() {
+					if _, err := addr.ValueForProtocol(ma.P_QUIC_V1); err == nil {
+						netw, addr, err := manet.DialArgs(addr)
+						if err != nil {
+							return false
+						}
+						quicAddrPorts[netw+"_"+addr] = struct{}{}
+					}
+				}
+				_, ok := quicAddrPorts[network+"_"+laddr.String()]
+				return ok
+			}
+
+			return func(network string, laddr *net.UDPAddr) (net.PacketConn, error) {
+				if hasQuicAddrPortFor(network, laddr) {
+					return cm.SharedNonQUICPacketConn(network, laddr)
+				}
+				return net.ListenUDP(network, laddr)
+			}
+		}),
 	}
 	fxopts = append(fxopts, cfg.Transports...)
 	if cfg.Insecure {
diff --git a/core/crypto/pb/crypto.pb.go b/core/crypto/pb/crypto.pb.go
index 8f48c7bb..0b406794 100644
--- a/core/crypto/pb/crypto.pb.go
+++ b/core/crypto/pb/crypto.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
 // source: pb/crypto.proto
 
 package pb
@@ -229,7 +229,7 @@ func file_pb_crypto_proto_rawDescGZIP() []byte {
 
 var file_pb_crypto_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
 var file_pb_crypto_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
-var file_pb_crypto_proto_goTypes = []any{
+var file_pb_crypto_proto_goTypes = []interface{}{
 	(KeyType)(0),       // 0: crypto.pb.KeyType
 	(*PublicKey)(nil),  // 1: crypto.pb.PublicKey
 	(*PrivateKey)(nil), // 2: crypto.pb.PrivateKey
@@ -250,7 +250,7 @@ func file_pb_crypto_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_pb_crypto_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_pb_crypto_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*PublicKey); i {
 			case 0:
 				return &v.state
@@ -262,7 +262,7 @@ func file_pb_crypto_proto_init() {
 				return nil
 			}
 		}
-		file_pb_crypto_proto_msgTypes[1].Exporter = func(v any, i int) any {
+		file_pb_crypto_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*PrivateKey); i {
 			case 0:
 				return &v.state
diff --git a/core/peer/pb/peer_record.pb.go b/core/peer/pb/peer_record.pb.go
index 9b37df65..2aa8a07a 100644
--- a/core/peer/pb/peer_record.pb.go
+++ b/core/peer/pb/peer_record.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
 // source: pb/peer_record.proto
 
 package pb
@@ -174,7 +174,7 @@ func file_pb_peer_record_proto_rawDescGZIP() []byte {
 }
 
 var file_pb_peer_record_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
-var file_pb_peer_record_proto_goTypes = []any{
+var file_pb_peer_record_proto_goTypes = []interface{}{
 	(*PeerRecord)(nil),             // 0: peer.pb.PeerRecord
 	(*PeerRecord_AddressInfo)(nil), // 1: peer.pb.PeerRecord.AddressInfo
 }
@@ -193,7 +193,7 @@ func file_pb_peer_record_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_pb_peer_record_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_pb_peer_record_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*PeerRecord); i {
 			case 0:
 				return &v.state
@@ -205,7 +205,7 @@ func file_pb_peer_record_proto_init() {
 				return nil
 			}
 		}
-		file_pb_peer_record_proto_msgTypes[1].Exporter = func(v any, i int) any {
+		file_pb_peer_record_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*PeerRecord_AddressInfo); i {
 			case 0:
 				return &v.state
diff --git a/core/record/pb/envelope.pb.go b/core/record/pb/envelope.pb.go
index 58a1be1d..1d3c7f25 100644
--- a/core/record/pb/envelope.pb.go
+++ b/core/record/pb/envelope.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
 // source: pb/envelope.proto
 
 package pb
@@ -139,7 +139,7 @@ func file_pb_envelope_proto_rawDescGZIP() []byte {
 }
 
 var file_pb_envelope_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
-var file_pb_envelope_proto_goTypes = []any{
+var file_pb_envelope_proto_goTypes = []interface{}{
 	(*Envelope)(nil),     // 0: record.pb.Envelope
 	(*pb.PublicKey)(nil), // 1: crypto.pb.PublicKey
 }
@@ -158,7 +158,7 @@ func file_pb_envelope_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_pb_envelope_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_pb_envelope_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Envelope); i {
 			case 0:
 				return &v.state
diff --git a/core/sec/insecure/pb/plaintext.pb.go b/core/sec/insecure/pb/plaintext.pb.go
index 35d46a0b..16b910b4 100644
--- a/core/sec/insecure/pb/plaintext.pb.go
+++ b/core/sec/insecure/pb/plaintext.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
 // source: pb/plaintext.proto
 
 package pb
@@ -103,7 +103,7 @@ func file_pb_plaintext_proto_rawDescGZIP() []byte {
 }
 
 var file_pb_plaintext_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
-var file_pb_plaintext_proto_goTypes = []any{
+var file_pb_plaintext_proto_goTypes = []interface{}{
 	(*Exchange)(nil),     // 0: plaintext.pb.Exchange
 	(*pb.PublicKey)(nil), // 1: crypto.pb.PublicKey
 }
@@ -122,7 +122,7 @@ func file_pb_plaintext_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_pb_plaintext_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_pb_plaintext_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Exchange); i {
 			case 0:
 				return &v.state
diff --git a/p2p/host/autonat/pb/autonat.pb.go b/p2p/host/autonat/pb/autonat.pb.go
index 41df2cb7..2764883f 100644
--- a/p2p/host/autonat/pb/autonat.pb.go
+++ b/p2p/host/autonat/pb/autonat.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
 // source: pb/autonat.proto
 
 package pb
@@ -426,7 +426,7 @@ func file_pb_autonat_proto_rawDescGZIP() []byte {
 
 var file_pb_autonat_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
 var file_pb_autonat_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
-var file_pb_autonat_proto_goTypes = []any{
+var file_pb_autonat_proto_goTypes = []interface{}{
 	(Message_MessageType)(0),     // 0: autonat.pb.Message.MessageType
 	(Message_ResponseStatus)(0),  // 1: autonat.pb.Message.ResponseStatus
 	(*Message)(nil),              // 2: autonat.pb.Message
@@ -453,7 +453,7 @@ func file_pb_autonat_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_pb_autonat_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_pb_autonat_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Message); i {
 			case 0:
 				return &v.state
@@ -465,7 +465,7 @@ func file_pb_autonat_proto_init() {
 				return nil
 			}
 		}
-		file_pb_autonat_proto_msgTypes[1].Exporter = func(v any, i int) any {
+		file_pb_autonat_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Message_PeerInfo); i {
 			case 0:
 				return &v.state
@@ -477,7 +477,7 @@ func file_pb_autonat_proto_init() {
 				return nil
 			}
 		}
-		file_pb_autonat_proto_msgTypes[2].Exporter = func(v any, i int) any {
+		file_pb_autonat_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Message_Dial); i {
 			case 0:
 				return &v.state
@@ -489,7 +489,7 @@ func file_pb_autonat_proto_init() {
 				return nil
 			}
 		}
-		file_pb_autonat_proto_msgTypes[3].Exporter = func(v any, i int) any {
+		file_pb_autonat_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Message_DialResponse); i {
 			case 0:
 				return &v.state
diff --git a/p2p/host/peerstore/pstoreds/pb/pstore.pb.go b/p2p/host/peerstore/pstoreds/pb/pstore.pb.go
index aacbb367..43c620c0 100644
--- a/p2p/host/peerstore/pstoreds/pb/pstore.pb.go
+++ b/p2p/host/peerstore/pstoreds/pb/pstore.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
 // source: pb/pstore.proto
 
 package pb
@@ -252,7 +252,7 @@ func file_pb_pstore_proto_rawDescGZIP() []byte {
 }
 
 var file_pb_pstore_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
-var file_pb_pstore_proto_goTypes = []any{
+var file_pb_pstore_proto_goTypes = []interface{}{
 	(*AddrBookRecord)(nil),                 // 0: pstore.pb.AddrBookRecord
 	(*AddrBookRecord_AddrEntry)(nil),       // 1: pstore.pb.AddrBookRecord.AddrEntry
 	(*AddrBookRecord_CertifiedRecord)(nil), // 2: pstore.pb.AddrBookRecord.CertifiedRecord
@@ -273,7 +273,7 @@ func file_pb_pstore_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_pb_pstore_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_pb_pstore_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*AddrBookRecord); i {
 			case 0:
 				return &v.state
@@ -285,7 +285,7 @@ func file_pb_pstore_proto_init() {
 				return nil
 			}
 		}
-		file_pb_pstore_proto_msgTypes[1].Exporter = func(v any, i int) any {
+		file_pb_pstore_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*AddrBookRecord_AddrEntry); i {
 			case 0:
 				return &v.state
@@ -297,7 +297,7 @@ func file_pb_pstore_proto_init() {
 				return nil
 			}
 		}
-		file_pb_pstore_proto_msgTypes[2].Exporter = func(v any, i int) any {
+		file_pb_pstore_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*AddrBookRecord_CertifiedRecord); i {
 			case 0:
 				return &v.state
diff --git a/p2p/net/swarm/swarm_listen.go b/p2p/net/swarm/swarm_listen.go
index 0905e845..e94db44a 100644
--- a/p2p/net/swarm/swarm_listen.go
+++ b/p2p/net/swarm/swarm_listen.go
@@ -3,6 +3,7 @@ package swarm
 import (
 	"errors"
 	"fmt"
+	"slices"
 	"time"
 
 	"github.com/libp2p/go-libp2p/core/canonicallog"
@@ -12,13 +13,44 @@ import (
 	ma "github.com/multiformats/go-multiaddr"
 )
 
+type OrderedListener interface {
+	// Transports optionally implement this interface to indicate the relative
+	// ordering that listeners should be setup. Some transports may optionally
+	// make use of other listeners if they are setup. e.g. WebRTC may reuse the
+	// same UDP port as QUIC, but only when QUIC is setup first.
+	// lower values are setup first.
+	ListenOrder() int
+}
+
 // Listen sets up listeners for all of the given addresses.
 // It returns as long as we successfully listen on at least *one* address.
 func (s *Swarm) Listen(addrs ...ma.Multiaddr) error {
 	errs := make([]error, len(addrs))
 	var succeeded int
-	for i, a := range addrs {
-		if err := s.AddListenAddr(a); err != nil {
+
+	type addrAndListener struct {
+		addr ma.Multiaddr
+		lTpt transport.Transport
+	}
+	sortedAddrsAndTpts := make([]addrAndListener, 0, len(addrs))
+	for _, a := range addrs {
+		t := s.TransportForListening(a)
+		sortedAddrsAndTpts = append(sortedAddrsAndTpts, addrAndListener{addr: a, lTpt: t})
+	}
+	slices.SortFunc(sortedAddrsAndTpts, func(a, b addrAndListener) int {
+		aOrder := 0
+		bOrder := 0
+		if l, ok := a.lTpt.(OrderedListener); ok {
+			aOrder = l.ListenOrder()
+		}
+		if l, ok := b.lTpt.(OrderedListener); ok {
+			bOrder = l.ListenOrder()
+		}
+		return aOrder - bOrder
+	})
+
+	for i, a := range sortedAddrsAndTpts {
+		if err := s.AddListenAddr(a.addr); err != nil {
 			errs[i] = err
 		} else {
 			succeeded++
@@ -27,11 +59,11 @@ func (s *Swarm) Listen(addrs ...ma.Multiaddr) error {
 
 	for i, e := range errs {
 		if e != nil {
-			log.Warnw("listening failed", "on", addrs[i], "error", errs[i])
+			log.Warnw("listening failed", "on", sortedAddrsAndTpts[i].addr, "error", errs[i])
 		}
 	}
 
-	if succeeded == 0 && len(addrs) > 0 {
+	if succeeded == 0 && len(sortedAddrsAndTpts) > 0 {
 		return fmt.Errorf("failed to listen on any addresses: %s", errs)
 	}
 
diff --git a/p2p/protocol/autonatv2/pb/autonatv2.pb.go b/p2p/protocol/autonatv2/pb/autonatv2.pb.go
index b4cb7959..5c3ea808 100644
--- a/p2p/protocol/autonatv2/pb/autonatv2.pb.go
+++ b/p2p/protocol/autonatv2/pb/autonatv2.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v4.25.3
 // source: pb/autonatv2.proto
 
 package pb
@@ -672,7 +672,7 @@ func file_pb_autonatv2_proto_rawDescGZIP() []byte {
 
 var file_pb_autonatv2_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
 var file_pb_autonatv2_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
-var file_pb_autonatv2_proto_goTypes = []any{
+var file_pb_autonatv2_proto_goTypes = []interface{}{
 	(DialStatus)(0),                      // 0: autonatv2.pb.DialStatus
 	(DialResponse_ResponseStatus)(0),     // 1: autonatv2.pb.DialResponse.ResponseStatus
 	(DialBackResponse_DialBackStatus)(0), // 2: autonatv2.pb.DialBackResponse.DialBackStatus
@@ -705,7 +705,7 @@ func file_pb_autonatv2_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_pb_autonatv2_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_pb_autonatv2_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Message); i {
 			case 0:
 				return &v.state
@@ -717,7 +717,7 @@ func file_pb_autonatv2_proto_init() {
 				return nil
 			}
 		}
-		file_pb_autonatv2_proto_msgTypes[1].Exporter = func(v any, i int) any {
+		file_pb_autonatv2_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*DialRequest); i {
 			case 0:
 				return &v.state
@@ -729,7 +729,7 @@ func file_pb_autonatv2_proto_init() {
 				return nil
 			}
 		}
-		file_pb_autonatv2_proto_msgTypes[2].Exporter = func(v any, i int) any {
+		file_pb_autonatv2_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*DialDataRequest); i {
 			case 0:
 				return &v.state
@@ -741,7 +741,7 @@ func file_pb_autonatv2_proto_init() {
 				return nil
 			}
 		}
-		file_pb_autonatv2_proto_msgTypes[3].Exporter = func(v any, i int) any {
+		file_pb_autonatv2_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*DialResponse); i {
 			case 0:
 				return &v.state
@@ -753,7 +753,7 @@ func file_pb_autonatv2_proto_init() {
 				return nil
 			}
 		}
-		file_pb_autonatv2_proto_msgTypes[4].Exporter = func(v any, i int) any {
+		file_pb_autonatv2_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*DialDataResponse); i {
 			case 0:
 				return &v.state
@@ -765,7 +765,7 @@ func file_pb_autonatv2_proto_init() {
 				return nil
 			}
 		}
-		file_pb_autonatv2_proto_msgTypes[5].Exporter = func(v any, i int) any {
+		file_pb_autonatv2_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*DialBack); i {
 			case 0:
 				return &v.state
@@ -777,7 +777,7 @@ func file_pb_autonatv2_proto_init() {
 				return nil
 			}
 		}
-		file_pb_autonatv2_proto_msgTypes[6].Exporter = func(v any, i int) any {
+		file_pb_autonatv2_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*DialBackResponse); i {
 			case 0:
 				return &v.state
@@ -790,7 +790,7 @@ func file_pb_autonatv2_proto_init() {
 			}
 		}
 	}
-	file_pb_autonatv2_proto_msgTypes[0].OneofWrappers = []any{
+	file_pb_autonatv2_proto_msgTypes[0].OneofWrappers = []interface{}{
 		(*Message_DialRequest)(nil),
 		(*Message_DialResponse)(nil),
 		(*Message_DialDataRequest)(nil),
diff --git a/p2p/protocol/circuitv2/pb/circuit.pb.go b/p2p/protocol/circuitv2/pb/circuit.pb.go
index afe93577..d4d285a3 100644
--- a/p2p/protocol/circuitv2/pb/circuit.pb.go
+++ b/p2p/protocol/circuitv2/pb/circuit.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
 // source: pb/circuit.proto
 
 package pb
@@ -606,7 +606,7 @@ func file_pb_circuit_proto_rawDescGZIP() []byte {
 
 var file_pb_circuit_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
 var file_pb_circuit_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
-var file_pb_circuit_proto_goTypes = []any{
+var file_pb_circuit_proto_goTypes = []interface{}{
 	(Status)(0),           // 0: circuit.pb.Status
 	(HopMessage_Type)(0),  // 1: circuit.pb.HopMessage.Type
 	(StopMessage_Type)(0), // 2: circuit.pb.StopMessage.Type
@@ -639,7 +639,7 @@ func file_pb_circuit_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_pb_circuit_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_pb_circuit_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*HopMessage); i {
 			case 0:
 				return &v.state
@@ -651,7 +651,7 @@ func file_pb_circuit_proto_init() {
 				return nil
 			}
 		}
-		file_pb_circuit_proto_msgTypes[1].Exporter = func(v any, i int) any {
+		file_pb_circuit_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*StopMessage); i {
 			case 0:
 				return &v.state
@@ -663,7 +663,7 @@ func file_pb_circuit_proto_init() {
 				return nil
 			}
 		}
-		file_pb_circuit_proto_msgTypes[2].Exporter = func(v any, i int) any {
+		file_pb_circuit_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Peer); i {
 			case 0:
 				return &v.state
@@ -675,7 +675,7 @@ func file_pb_circuit_proto_init() {
 				return nil
 			}
 		}
-		file_pb_circuit_proto_msgTypes[3].Exporter = func(v any, i int) any {
+		file_pb_circuit_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Reservation); i {
 			case 0:
 				return &v.state
@@ -687,7 +687,7 @@ func file_pb_circuit_proto_init() {
 				return nil
 			}
 		}
-		file_pb_circuit_proto_msgTypes[4].Exporter = func(v any, i int) any {
+		file_pb_circuit_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Limit); i {
 			case 0:
 				return &v.state
@@ -700,11 +700,11 @@ func file_pb_circuit_proto_init() {
 			}
 		}
 	}
-	file_pb_circuit_proto_msgTypes[0].OneofWrappers = []any{}
-	file_pb_circuit_proto_msgTypes[1].OneofWrappers = []any{}
-	file_pb_circuit_proto_msgTypes[2].OneofWrappers = []any{}
-	file_pb_circuit_proto_msgTypes[3].OneofWrappers = []any{}
-	file_pb_circuit_proto_msgTypes[4].OneofWrappers = []any{}
+	file_pb_circuit_proto_msgTypes[0].OneofWrappers = []interface{}{}
+	file_pb_circuit_proto_msgTypes[1].OneofWrappers = []interface{}{}
+	file_pb_circuit_proto_msgTypes[2].OneofWrappers = []interface{}{}
+	file_pb_circuit_proto_msgTypes[3].OneofWrappers = []interface{}{}
+	file_pb_circuit_proto_msgTypes[4].OneofWrappers = []interface{}{}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
diff --git a/p2p/protocol/circuitv2/pb/voucher.pb.go b/p2p/protocol/circuitv2/pb/voucher.pb.go
index 6b02cdac..917b5370 100644
--- a/p2p/protocol/circuitv2/pb/voucher.pb.go
+++ b/p2p/protocol/circuitv2/pb/voucher.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
 // source: pb/voucher.proto
 
 package pb
@@ -115,7 +115,7 @@ func file_pb_voucher_proto_rawDescGZIP() []byte {
 }
 
 var file_pb_voucher_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
-var file_pb_voucher_proto_goTypes = []any{
+var file_pb_voucher_proto_goTypes = []interface{}{
 	(*ReservationVoucher)(nil), // 0: circuit.pb.ReservationVoucher
 }
 var file_pb_voucher_proto_depIdxs = []int32{
@@ -132,7 +132,7 @@ func file_pb_voucher_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_pb_voucher_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_pb_voucher_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*ReservationVoucher); i {
 			case 0:
 				return &v.state
@@ -145,7 +145,7 @@ func file_pb_voucher_proto_init() {
 			}
 		}
 	}
-	file_pb_voucher_proto_msgTypes[0].OneofWrappers = []any{}
+	file_pb_voucher_proto_msgTypes[0].OneofWrappers = []interface{}{}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
diff --git a/p2p/protocol/holepunch/pb/holepunch.pb.go b/p2p/protocol/holepunch/pb/holepunch.pb.go
index 8478ab69..ca568580 100644
--- a/p2p/protocol/holepunch/pb/holepunch.pb.go
+++ b/p2p/protocol/holepunch/pb/holepunch.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
 // source: pb/holepunch.proto
 
 package pb
@@ -161,7 +161,7 @@ func file_pb_holepunch_proto_rawDescGZIP() []byte {
 
 var file_pb_holepunch_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
 var file_pb_holepunch_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
-var file_pb_holepunch_proto_goTypes = []any{
+var file_pb_holepunch_proto_goTypes = []interface{}{
 	(HolePunch_Type)(0), // 0: holepunch.pb.HolePunch.Type
 	(*HolePunch)(nil),   // 1: holepunch.pb.HolePunch
 }
@@ -180,7 +180,7 @@ func file_pb_holepunch_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_pb_holepunch_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_pb_holepunch_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*HolePunch); i {
 			case 0:
 				return &v.state
diff --git a/p2p/protocol/identify/pb/identify.pb.go b/p2p/protocol/identify/pb/identify.pb.go
index 078a7e17..1c93815d 100644
--- a/p2p/protocol/identify/pb/identify.pb.go
+++ b/p2p/protocol/identify/pb/identify.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
 // source: pb/identify.proto
 
 package pb
@@ -168,7 +168,7 @@ func file_pb_identify_proto_rawDescGZIP() []byte {
 }
 
 var file_pb_identify_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
-var file_pb_identify_proto_goTypes = []any{
+var file_pb_identify_proto_goTypes = []interface{}{
 	(*Identify)(nil), // 0: identify.pb.Identify
 }
 var file_pb_identify_proto_depIdxs = []int32{
@@ -185,7 +185,7 @@ func file_pb_identify_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_pb_identify_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_pb_identify_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Identify); i {
 			case 0:
 				return &v.state
diff --git a/p2p/security/noise/pb/payload.pb.go b/p2p/security/noise/pb/payload.pb.go
index dc980dd1..8e3a805a 100644
--- a/p2p/security/noise/pb/payload.pb.go
+++ b/p2p/security/noise/pb/payload.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
 // source: pb/payload.proto
 
 package pb
@@ -174,7 +174,7 @@ func file_pb_payload_proto_rawDescGZIP() []byte {
 }
 
 var file_pb_payload_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
-var file_pb_payload_proto_goTypes = []any{
+var file_pb_payload_proto_goTypes = []interface{}{
 	(*NoiseExtensions)(nil),       // 0: pb.NoiseExtensions
 	(*NoiseHandshakePayload)(nil), // 1: pb.NoiseHandshakePayload
 }
@@ -193,7 +193,7 @@ func file_pb_payload_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_pb_payload_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_pb_payload_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*NoiseExtensions); i {
 			case 0:
 				return &v.state
@@ -205,7 +205,7 @@ func file_pb_payload_proto_init() {
 				return nil
 			}
 		}
-		file_pb_payload_proto_msgTypes[1].Exporter = func(v any, i int) any {
+		file_pb_payload_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*NoiseHandshakePayload); i {
 			case 0:
 				return &v.state
diff --git a/p2p/transport/quic/transport.go b/p2p/transport/quic/transport.go
index 18d198bb..04b5e4d6 100644
--- a/p2p/transport/quic/transport.go
+++ b/p2p/transport/quic/transport.go
@@ -26,6 +26,8 @@ import (
 	"github.com/quic-go/quic-go"
 )
 
+const ListenOrder = 1
+
 var log = logging.Logger("quic-transport")
 
 var ErrHolePunching = errors.New("hole punching attempted; no active dial")
@@ -103,6 +105,10 @@ func NewTransport(key ic.PrivKey, connManager *quicreuse.ConnManager, psk pnet.P
 	}, nil
 }
 
+func (t *transport) ListenOrder() int {
+	return ListenOrder
+}
+
 // Dial dials a new QUIC connection
 func (t *transport) Dial(ctx context.Context, raddr ma.Multiaddr, p peer.ID) (_c tpt.CapableConn, _err error) {
 	if ok, isClient, _ := network.GetSimultaneousConnect(ctx); ok && !isClient {
diff --git a/p2p/transport/quicreuse/connmgr.go b/p2p/transport/quicreuse/connmgr.go
index 500a9c97..01e1bcda 100644
--- a/p2p/transport/quicreuse/connmgr.go
+++ b/p2p/transport/quicreuse/connmgr.go
@@ -154,6 +154,28 @@ func (c *ConnManager) onListenerClosed(key string) {
 	}
 }
 
+func (c *ConnManager) SharedNonQUICPacketConn(network string, laddr *net.UDPAddr) (net.PacketConn, error) {
+	c.quicListenersMu.Lock()
+	defer c.quicListenersMu.Unlock()
+	key := laddr.String()
+	entry, ok := c.quicListeners[key]
+	if !ok {
+		return nil, errors.New("expected to be able to share with a QUIC listener, but no QUIC listener found. The QUIC listener should start first")
+	}
+	t := entry.ln.transport
+	if t, ok := t.(*refcountedTransport); ok {
+		t.IncreaseCount()
+		ctx, cancel := context.WithCancel(context.Background())
+		return &nonQUICPacketConn{
+			ctx:             ctx,
+			ctxCancel:       cancel,
+			owningTransport: t,
+			tr:              &t.Transport,
+		}, nil
+	}
+	return nil, errors.New("expected to be able to share with a QUIC listener, but the QUIC listener is not using a refcountedTransport. `DisableReuseport` should not be set")
+}
+
 func (c *ConnManager) transportForListen(network string, laddr *net.UDPAddr) (refCountedQuicTransport, error) {
 	if c.enableReuseport {
 		reuse, err := c.getReuse(network)
diff --git a/p2p/transport/quicreuse/nonquic_packetconn.go b/p2p/transport/quicreuse/nonquic_packetconn.go
new file mode 100644
index 00000000..2f950e76
--- /dev/null
+++ b/p2p/transport/quicreuse/nonquic_packetconn.go
@@ -0,0 +1,74 @@
+package quicreuse
+
+import (
+	"context"
+	"net"
+	"time"
+
+	"github.com/quic-go/quic-go"
+)
+
+// nonQUICPacketConn is a net.PacketConn that can be used to read and write
+// non-QUIC packets on a quic.Transport. This lets us reuse this UDP port for
+// other transports like WebRTC.
+type nonQUICPacketConn struct {
+	owningTransport refCountedQuicTransport
+	tr              *quic.Transport
+	ctx             context.Context
+	ctxCancel       context.CancelFunc
+	readCtx         context.Context
+	readCancel      context.CancelFunc
+}
+
+// Close implements net.PacketConn.
+func (n *nonQUICPacketConn) Close() error {
+	n.ctxCancel()
+
+	// Don't actually close the underlying transport since someone else might be using it.
+	// reuse has it's own gc to close unused transports.
+	n.owningTransport.DecreaseCount()
+	return nil
+}
+
+// LocalAddr implements net.PacketConn.
+func (n *nonQUICPacketConn) LocalAddr() net.Addr {
+	return n.tr.Conn.LocalAddr()
+}
+
+// ReadFrom implements net.PacketConn.
+func (n *nonQUICPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
+	ctx := n.readCtx
+	if ctx == nil {
+		ctx = n.ctx
+	}
+	return n.tr.ReadNonQUICPacket(ctx, p)
+}
+
+// SetDeadline implements net.PacketConn.
+func (n *nonQUICPacketConn) SetDeadline(t time.Time) error {
+	// Only used for reads.
+	return n.SetReadDeadline(t)
+}
+
+// SetReadDeadline implements net.PacketConn.
+func (n *nonQUICPacketConn) SetReadDeadline(t time.Time) error {
+	if t.IsZero() && n.readCtx != nil {
+		n.readCancel()
+		n.readCtx = nil
+	}
+	n.readCtx, n.readCancel = context.WithDeadline(n.ctx, t)
+	return nil
+}
+
+// SetWriteDeadline implements net.PacketConn.
+func (n *nonQUICPacketConn) SetWriteDeadline(t time.Time) error {
+	// Unused. quic-go doesn't support deadlines for writes.
+	return nil
+}
+
+// WriteTo implements net.PacketConn.
+func (n *nonQUICPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
+	return n.tr.WriteTo(p, addr)
+}
+
+var _ net.PacketConn = &nonQUICPacketConn{}
diff --git a/p2p/transport/webrtc/pb/message.pb.go b/p2p/transport/webrtc/pb/message.pb.go
index 44e787cd..384bddd2 100644
--- a/p2p/transport/webrtc/pb/message.pb.go
+++ b/p2p/transport/webrtc/pb/message.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.34.2
-// 	protoc        v5.27.2
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
 // source: message.proto
 
 package pb
@@ -177,7 +177,7 @@ func file_message_proto_rawDescGZIP() []byte {
 
 var file_message_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
 var file_message_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
-var file_message_proto_goTypes = []any{
+var file_message_proto_goTypes = []interface{}{
 	(Message_Flag)(0), // 0: Message.Flag
 	(*Message)(nil),   // 1: Message
 }
@@ -196,7 +196,7 @@ func file_message_proto_init() {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_message_proto_msgTypes[0].Exporter = func(v any, i int) any {
+		file_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Message); i {
 			case 0:
 				return &v.state
diff --git a/p2p/transport/webrtc/transport.go b/p2p/transport/webrtc/transport.go
index db605bcc..8aaa93e4 100644
--- a/p2p/transport/webrtc/transport.go
+++ b/p2p/transport/webrtc/transport.go
@@ -26,6 +26,7 @@ import (
 	"github.com/libp2p/go-libp2p/core/sec"
 	tpt "github.com/libp2p/go-libp2p/core/transport"
 	"github.com/libp2p/go-libp2p/p2p/security/noise"
+	libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic"
 	"github.com/libp2p/go-libp2p/p2p/transport/webrtc/pb"
 	"github.com/libp2p/go-msgio"
 
@@ -78,6 +79,8 @@ type WebRTCTransport struct {
 	noiseTpt     *noise.Transport
 	localPeerId  peer.ID
 
+	listenUDP func(network string, laddr *net.UDPAddr) (net.PacketConn, error)
+
 	// timeouts
 	peerConnectionTimeouts iceTimeouts
 
@@ -95,7 +98,9 @@ type iceTimeouts struct {
 	Keepalive  time.Duration
 }
 
-func New(privKey ic.PrivKey, psk pnet.PSK, gater connmgr.ConnectionGater, rcmgr network.ResourceManager, opts ...Option) (*WebRTCTransport, error) {
+type ListenUDPFn func(network string, laddr *net.UDPAddr) (net.PacketConn, error)
+
+func New(privKey ic.PrivKey, psk pnet.PSK, gater connmgr.ConnectionGater, rcmgr network.ResourceManager, listenUDP ListenUDPFn, opts ...Option) (*WebRTCTransport, error) {
 	if psk != nil {
 		log.Error("WebRTC doesn't support private networks yet.")
 		return nil, fmt.Errorf("WebRTC doesn't support private networks yet")
@@ -141,6 +146,7 @@ func New(privKey ic.PrivKey, psk pnet.PSK, gater connmgr.ConnectionGater, rcmgr
 		noiseTpt:     noiseTpt,
 		localPeerId:  localPeerID,
 
+		listenUDP: listenUDP,
 		peerConnectionTimeouts: iceTimeouts{
 			Disconnect: DefaultDisconnectedTimeout,
 			Failed:     DefaultFailedTimeout,
@@ -157,6 +163,10 @@ func New(privKey ic.PrivKey, psk pnet.PSK, gater connmgr.ConnectionGater, rcmgr
 	return transport, nil
 }
 
+func (t *WebRTCTransport) ListenOrder() int {
+	return libp2pquic.ListenOrder + 1 // We want to listen after QUIC listens so we can possibly reuse the same port.
+}
+
 func (t *WebRTCTransport) Protocols() []int {
 	return []int{ma.P_WEBRTC_DIRECT}
 }
@@ -190,7 +200,7 @@ func (t *WebRTCTransport) Listen(addr ma.Multiaddr) (tpt.Listener, error) {
 		return nil, fmt.Errorf("listener could not resolve udp address: %w", err)
 	}
 
-	socket, err := net.ListenUDP(nw, udpAddr)
+	socket, err := t.listenUDP(nw, udpAddr)
 	if err != nil {
 		return nil, fmt.Errorf("listen on udp: %w", err)
 	}
@@ -203,7 +213,7 @@ func (t *WebRTCTransport) Listen(addr ma.Multiaddr) (tpt.Listener, error) {
 	return listener, nil
 }
 
-func (t *WebRTCTransport) listenSocket(socket *net.UDPConn) (tpt.Listener, error) {
+func (t *WebRTCTransport) listenSocket(socket net.PacketConn) (tpt.Listener, error) {
 	listenerMultiaddr, err := manet.FromNetAddr(socket.LocalAddr())
 	if err != nil {
 		return nil, err


Test patch:
diff --git a/libp2p_test.go b/libp2p_test.go
index 54a1b668..676af769 100644
--- a/libp2p_test.go
+++ b/libp2p_test.go
@@ -6,6 +6,8 @@ import (
 	"errors"
 	"fmt"
 	"regexp"
+	"strconv"
+	"strings"
 	"testing"
 	"time"
 
@@ -24,7 +26,9 @@ import (
 	"github.com/libp2p/go-libp2p/p2p/security/noise"
 	tls "github.com/libp2p/go-libp2p/p2p/security/tls"
 	quic "github.com/libp2p/go-libp2p/p2p/transport/quic"
+	"github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
 	"github.com/libp2p/go-libp2p/p2p/transport/tcp"
+	libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc"
 	webtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
 	"go.uber.org/goleak"
 
@@ -416,6 +420,7 @@ func TestMain(m *testing.M) {
 		m,
 		// This will return eventually (5s timeout) but doesn't take a context.
 		goleak.IgnoreAnyFunction("github.com/koron/go-ssdp.Search"),
+		goleak.IgnoreAnyFunction("github.com/pion/sctp.(*Stream).SetReadDeadline.func1"),
 		// Logging & Stats
 		goleak.IgnoreTopFunction("github.com/ipfs/go-log/v2/writer.(*MirrorWriter).logRoutine"),
 		goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"),
@@ -490,3 +495,84 @@ func TestHostAddrsFactoryAddsCerthashes(t *testing.T) {
 	}, 5*time.Second, 50*time.Millisecond)
 	h.Close()
 }
+
+func TestWebRTCReuseAddrWithQUIC(t *testing.T) {
+	order := [][]string{
+		{"/ip4/127.0.0.1/udp/54322/quic-v1", "/ip4/127.0.0.1/udp/54322/webrtc-direct"},
+		{"/ip4/127.0.0.1/udp/54322/webrtc-direct", "/ip4/127.0.0.1/udp/54322/quic-v1"},
+		// We do not support WebRTC automatically reusing QUIC addresses if port is not specified, yet.
+		// {"/ip4/127.0.0.1/udp/0/webrtc-direct", "/ip4/127.0.0.1/udp/0/quic-v1"},
+	}
+	for i, addrs := range order {
+		t.Run("Order "+strconv.Itoa(i), func(t *testing.T) {
+			h1, err := New(ListenAddrStrings(addrs...), Transport(quic.NewTransport), Transport(libp2pwebrtc.New))
+			require.NoError(t, err)
+			defer h1.Close()
+
+			seenPorts := make(map[string]struct{})
+			for _, addr := range h1.Addrs() {
+				s, err := addr.ValueForProtocol(ma.P_UDP)
+				require.NoError(t, err)
+				seenPorts[s] = struct{}{}
+			}
+			require.Len(t, seenPorts, 1)
+
+			quicClient, err := New(NoListenAddrs, Transport(quic.NewTransport))
+			require.NoError(t, err)
+			defer quicClient.Close()
+
+			webrtcClient, err := New(NoListenAddrs, Transport(libp2pwebrtc.New))
+			require.NoError(t, err)
+			defer webrtcClient.Close()
+
+			for _, client := range []host.Host{quicClient, webrtcClient} {
+				err := client.Connect(context.Background(), peer.AddrInfo{ID: h1.ID(), Addrs: h1.Addrs()})
+				require.NoError(t, err)
+			}
+
+			t.Run("quic client can connect", func(t *testing.T) {
+				ctx, cancel := context.WithCancel(context.Background())
+				defer cancel()
+				p := ping.NewPingService(quicClient)
+				resCh := p.Ping(ctx, h1.ID())
+				res := <-resCh
+				require.NoError(t, res.Error)
+			})
+
+			t.Run("webrtc client can connect", func(t *testing.T) {
+				ctx, cancel := context.WithCancel(context.Background())
+				defer cancel()
+				p := ping.NewPingService(webrtcClient)
+				resCh := p.Ping(ctx, h1.ID())
+				res := <-resCh
+				require.NoError(t, res.Error)
+			})
+		})
+	}
+
+	swapPort := func(addrStrs []string, newPort string) []string {
+		out := make([]string, 0, len(addrStrs))
+		for _, addrStr := range addrStrs {
+			out = append(out, strings.Replace(addrStr, "54322", newPort, 1))
+		}
+		return out
+	}
+
+	t.Run("setup with no reuseport. Should fail", func(t *testing.T) {
+		h1, err := New(ListenAddrStrings(swapPort(order[0], "54323")...), Transport(quic.NewTransport), Transport(libp2pwebrtc.New), QUICReuse(quicreuse.NewConnManager, quicreuse.DisableReuseport()))
+		require.NoError(t, err) // It's a bug/feature that swarm.Listen does not error if at least one transport succeeds in listening.
+		defer h1.Close()
+		// Check that webrtc did fail to listen
+		require.Equal(t, 1, len(h1.Addrs()))
+		require.Contains(t, h1.Addrs()[0].String(), "quic-v1")
+	})
+
+	t.Run("setup with autonat", func(t *testing.T) {
+		h1, err := New(EnableAutoNATv2(), ListenAddrStrings(swapPort(order[0], "54324")...), Transport(quic.NewTransport), Transport(libp2pwebrtc.New), QUICReuse(quicreuse.NewConnManager, quicreuse.DisableReuseport()))
+		require.NoError(t, err) // It's a bug/feature that swarm.Listen does not error if at least one transport succeeds in listening.
+		defer h1.Close()
+		// Check that webrtc did fail to listen
+		require.Equal(t, 1, len(h1.Addrs()))
+		require.Contains(t, h1.Addrs()[0].String(), "quic-v1")
+	})
+}
diff --git a/p2p/transport/webrtc/transport_test.go b/p2p/transport/webrtc/transport_test.go
index e457c13f..e6a60420 100644
--- a/p2p/transport/webrtc/transport_test.go
+++ b/p2p/transport/webrtc/transport_test.go
@@ -29,12 +29,16 @@ import (
 	"golang.org/x/crypto/sha3"
 )
 
+var netListenUDP ListenUDPFn = func(network string, laddr *net.UDPAddr) (net.PacketConn, error) {
+	return net.ListenUDP(network, laddr)
+}
+
 func getTransport(t *testing.T, opts ...Option) (*WebRTCTransport, peer.ID) {
 	t.Helper()
 	privKey, _, err := crypto.GenerateKeyPair(crypto.Ed25519, -1)
 	require.NoError(t, err)
 	rcmgr := &network.NullResourceManager{}
-	transport, err := New(privKey, nil, nil, rcmgr, opts...)
+	transport, err := New(privKey, nil, nil, rcmgr, netListenUDP, opts...)
 	require.NoError(t, err)
 	peerID, err := peer.IDFromPrivateKey(privKey)
 	require.NoError(t, err)
@@ -45,7 +49,7 @@ func getTransport(t *testing.T, opts ...Option) (*WebRTCTransport, peer.ID) {
 func TestNullRcmgrTransport(t *testing.T) {
 	privKey, _, err := crypto.GenerateKeyPair(crypto.Ed25519, -1)
 	require.NoError(t, err)
-	transport, err := New(privKey, nil, nil, nil)
+	transport, err := New(privKey, nil, nil, nil, netListenUDP)
 	require.NoError(t, err)
 
 	listenTransport, pid := getTransport(t)