Repository: libp2p/go-libp2p
Pull request #1839 (2022-10-24 09:04:57)

holepunch: add multiaddress filter
This PR adds a new option to the holepunch service that allows filtering of multi addresses during the DCUtR protocol.

marten-seemann: Code LGTM, but CI still seems unhappy.

Patch:
diff --git a/README.md b/README.md
index 65880f74..7e2c48ad 100644
--- a/README.md
+++ b/README.md
@@ -19,9 +19,7 @@
 - [Usage](#usage)
   - [Examples](#examples)
 - [Development](#development)
-  - [Using the go-libp2p Workspace](#using-the-go-libp2p-workspace)
   - [Tests](#tests)
-  - [Packages](#packages)
 - [Contribute](#contribute)
 - [Supported Go Versions](#supported-go-versions)
 
diff --git a/config/config.go b/config/config.go
index adbcf1d1..ea674c0d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -14,7 +14,6 @@ import (
 	"github.com/libp2p/go-libp2p/core/peerstore"
 	"github.com/libp2p/go-libp2p/core/pnet"
 	"github.com/libp2p/go-libp2p/core/routing"
-	"github.com/libp2p/go-libp2p/core/sec"
 	"github.com/libp2p/go-libp2p/core/transport"
 	"github.com/libp2p/go-libp2p/p2p/host/autonat"
 	"github.com/libp2p/go-libp2p/p2p/host/autorelay"
@@ -28,13 +27,12 @@ import (
 	relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
 	"github.com/libp2p/go-libp2p/p2p/protocol/holepunch"
 
-	logging "github.com/ipfs/go-log/v2"
 	ma "github.com/multiformats/go-multiaddr"
 	madns "github.com/multiformats/go-multiaddr-dns"
+	"go.uber.org/fx"
+	"go.uber.org/fx/fxevent"
 )
 
-var log = logging.Logger("p2p-config")
-
 // AddrsFactory is a function that takes a set of multiaddrs we're listening on and
 // returns the set of multiaddrs we should advertise to the network.
 type AddrsFactory = bhost.AddrsFactory
@@ -71,9 +69,9 @@ type Config struct {
 
 	PeerKey crypto.PrivKey
 
-	Transports         []TptC
-	Muxers             []MsMuxC
-	SecurityTransports []MsSecC
+	Transports         []fx.Option
+	Muxers             []tptu.StreamMuxer
+	SecurityTransports []fx.Option
 	Insecure           bool
 	PSK                pnet.PSK
 
@@ -168,51 +166,56 @@ func (cfg *Config) addTransports(h host.Host) error {
 		// Should probably skip this if no transports.
 		return fmt.Errorf("swarm does not support transports")
 	}
-	var secure sec.SecureMuxer
+
+	var security []fx.Option
 	if cfg.Insecure {
-		secure = makeInsecureTransport(h.ID(), cfg.PeerKey)
+		security = append(security, fx.Provide(makeInsecureTransport))
 	} else {
-		var err error
-		secure, err = makeSecurityMuxer(h, cfg.SecurityTransports, cfg.Muxers)
-		if err != nil {
-			return err
-		}
-	}
-	muxer, err := makeMuxer(h, cfg.Muxers)
-	if err != nil {
-		return err
-	}
-	var opts []tptu.Option
-	if len(cfg.PSK) > 0 {
-		opts = append(opts, tptu.WithPSK(cfg.PSK))
-	}
-	if cfg.ConnectionGater != nil {
-		opts = append(opts, tptu.WithConnectionGater(cfg.ConnectionGater))
-	}
-	if cfg.ResourceManager != nil {
-		opts = append(opts, tptu.WithResourceManager(cfg.ResourceManager))
-	}
-	upgrader, err := tptu.New(secure, muxer, opts...)
-	if err != nil {
-		return err
+		security = cfg.SecurityTransports
+	}
+
+	fxopts := []fx.Option{
+		fx.WithLogger(func() fxevent.Logger { return getFXLogger() }),
+		fx.Provide(tptu.New),
+		fx.Provide(fx.Annotate(
+			makeSecurityMuxer,
+			fx.ParamTags(`group:"security"`),
+		)),
+		fx.Supply(cfg.Muxers),
+		fx.Supply(h.ID()),
+		fx.Provide(func() host.Host { return h }),
+		fx.Provide(func() crypto.PrivKey { return h.Peerstore().PrivKey(h.ID()) }),
+		fx.Provide(func() connmgr.ConnectionGater { return cfg.ConnectionGater }),
+		fx.Provide(func() pnet.PSK { return cfg.PSK }),
+		fx.Provide(func() network.ResourceManager { return cfg.ResourceManager }),
+		fx.Provide(func() *madns.Resolver { return cfg.MultiaddrResolver }),
+	}
+	fxopts = append(fxopts, cfg.Transports...)
+	if !cfg.Insecure {
+		fxopts = append(fxopts, security...)
+	}
+
+	fxopts = append(fxopts, fx.Invoke(
+		fx.Annotate(
+			func(tpts []transport.Transport) error {
+				for _, t := range tpts {
+					if err := swrm.AddTransport(t); err != nil {
+						return err
+					}
+				}
+				return nil
+			},
+			fx.ParamTags(`group:"transport"`),
+		)),
+	)
+	if cfg.Relay {
+		fxopts = append(fxopts, fx.Invoke(circuitv2.AddTransport))
 	}
-	tpts, err := makeTransports(h, upgrader, cfg.ConnectionGater, cfg.PSK, cfg.ResourceManager, cfg.MultiaddrResolver, cfg.Transports)
-	if err != nil {
+	app := fx.New(fxopts...)
+	if err := app.Err(); err != nil {
+		h.Close()
 		return err
 	}
-	for _, t := range tpts {
-		if err := swrm.AddTransport(t); err != nil {
-			return err
-		}
-	}
-
-	if cfg.Relay {
-		if err := circuitv2.AddTransport(h, upgrader); err != nil {
-			h.Close()
-			return err
-		}
-	}
-
 	return nil
 }
 
diff --git a/config/constructor_types.go b/config/constructor_types.go
deleted file mode 100644
index 9c14df2e..00000000
--- a/config/constructor_types.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package config
-
-import (
-	"fmt"
-	"reflect"
-
-	"github.com/libp2p/go-libp2p/core/connmgr"
-	"github.com/libp2p/go-libp2p/core/crypto"
-	"github.com/libp2p/go-libp2p/core/host"
-	"github.com/libp2p/go-libp2p/core/network"
-	"github.com/libp2p/go-libp2p/core/peer"
-	"github.com/libp2p/go-libp2p/core/peerstore"
-	"github.com/libp2p/go-libp2p/core/pnet"
-	"github.com/libp2p/go-libp2p/core/protocol"
-	"github.com/libp2p/go-libp2p/core/sec"
-	"github.com/libp2p/go-libp2p/core/transport"
-
-	madns "github.com/multiformats/go-multiaddr-dns"
-)
-
-var (
-	// interfaces
-	hostType      = reflect.TypeOf((*host.Host)(nil)).Elem()
-	networkType   = reflect.TypeOf((*network.Network)(nil)).Elem()
-	transportType = reflect.TypeOf((*transport.Transport)(nil)).Elem()
-	muxType       = reflect.TypeOf((*network.Multiplexer)(nil)).Elem()
-	securityType  = reflect.TypeOf((*sec.SecureTransport)(nil)).Elem()
-	privKeyType   = reflect.TypeOf((*crypto.PrivKey)(nil)).Elem()
-	pubKeyType    = reflect.TypeOf((*crypto.PubKey)(nil)).Elem()
-	pstoreType    = reflect.TypeOf((*peerstore.Peerstore)(nil)).Elem()
-	connGaterType = reflect.TypeOf((*connmgr.ConnectionGater)(nil)).Elem()
-	upgraderType  = reflect.TypeOf((*transport.Upgrader)(nil)).Elem()
-	rcmgrType     = reflect.TypeOf((*network.ResourceManager)(nil)).Elem()
-
-	// concrete types
-	peerIDType   = reflect.TypeOf((peer.ID)(""))
-	pskType      = reflect.TypeOf((pnet.PSK)(nil))
-	resolverType = reflect.TypeOf((*madns.Resolver)(nil))
-	muxersType   = reflect.TypeOf(([]protocol.ID)(nil))
-)
-
-var argTypes = map[reflect.Type]constructor{
-	upgraderType: func(_ host.Host, u transport.Upgrader, _ pnet.PSK, _ connmgr.ConnectionGater, _ network.ResourceManager, _ *madns.Resolver, _ []protocol.ID) interface{} {
-		return u
-	},
-	hostType: func(h host.Host, _ transport.Upgrader, _ pnet.PSK, _ connmgr.ConnectionGater, _ network.ResourceManager, _ *madns.Resolver, _ []protocol.ID) interface{} {
-		return h
-	},
-	networkType: func(h host.Host, _ transport.Upgrader, _ pnet.PSK, _ connmgr.ConnectionGater, _ network.ResourceManager, _ *madns.Resolver, _ []protocol.ID) interface{} {
-		return h.Network()
-	},
-	pskType: func(_ host.Host, _ transport.Upgrader, psk pnet.PSK, _ connmgr.ConnectionGater, _ network.ResourceManager, _ *madns.Resolver, _ []protocol.ID) interface{} {
-		return psk
-	},
-	connGaterType: func(_ host.Host, _ transport.Upgrader, _ pnet.PSK, cg connmgr.ConnectionGater, _ network.ResourceManager, _ *madns.Resolver, _ []protocol.ID) interface{} {
-		return cg
-	},
-	peerIDType: func(h host.Host, _ transport.Upgrader, _ pnet.PSK, _ connmgr.ConnectionGater, _ network.ResourceManager, _ *madns.Resolver, _ []protocol.ID) interface{} {
-		return h.ID()
-	},
-	privKeyType: func(h host.Host, _ transport.Upgrader, _ pnet.PSK, _ connmgr.ConnectionGater, _ network.ResourceManager, _ *madns.Resolver, _ []protocol.ID) interface{} {
-		return h.Peerstore().PrivKey(h.ID())
-	},
-	pubKeyType: func(h host.Host, _ transport.Upgrader, _ pnet.PSK, _ connmgr.ConnectionGater, _ network.ResourceManager, _ *madns.Resolver, _ []protocol.ID) interface{} {
-		return h.Peerstore().PubKey(h.ID())
-	},
-	pstoreType: func(h host.Host, _ transport.Upgrader, _ pnet.PSK, _ connmgr.ConnectionGater, _ network.ResourceManager, _ *madns.Resolver, _ []protocol.ID) interface{} {
-		return h.Peerstore()
-	},
-	rcmgrType: func(_ host.Host, _ transport.Upgrader, _ pnet.PSK, _ connmgr.ConnectionGater, rcmgr network.ResourceManager, _ *madns.Resolver, _ []protocol.ID) interface{} {
-		return rcmgr
-	},
-	resolverType: func(_ host.Host, _ transport.Upgrader, _ pnet.PSK, _ connmgr.ConnectionGater, _ network.ResourceManager, r *madns.Resolver, _ []protocol.ID) interface{} {
-		return r
-	},
-	muxersType: func(_ host.Host, _ transport.Upgrader, _ pnet.PSK, _ connmgr.ConnectionGater, _ network.ResourceManager, _ *madns.Resolver, muxers []protocol.ID) interface{} {
-		return muxers
-	},
-}
-
-func newArgTypeSet(types ...reflect.Type) map[reflect.Type]constructor {
-	result := make(map[reflect.Type]constructor, len(types))
-	for _, ty := range types {
-		c, ok := argTypes[ty]
-		if !ok {
-			panic(fmt.Sprintf("missing constructor for type %s", ty))
-		}
-		result[ty] = c
-	}
-	return result
-}
diff --git a/config/log.go b/config/log.go
new file mode 100644
index 00000000..3b74c38c
--- /dev/null
+++ b/config/log.go
@@ -0,0 +1,28 @@
+package config
+
+import (
+	"strings"
+	"sync"
+
+	logging "github.com/ipfs/go-log/v2"
+	"go.uber.org/fx/fxevent"
+)
+
+var log = logging.Logger("p2p-config")
+
+var (
+	fxLogger    fxevent.Logger
+	logInitOnce sync.Once
+)
+
+type fxLogWriter struct{}
+
+func (l *fxLogWriter) Write(b []byte) (int, error) {
+	log.Debug(strings.TrimSuffix(string(b), "\n"))
+	return len(b), nil
+}
+
+func getFXLogger() fxevent.Logger {
+	logInitOnce.Do(func() { fxLogger = &fxevent.ConsoleLogger{W: &fxLogWriter{}} })
+	return fxLogger
+}
diff --git a/config/muxer.go b/config/muxer.go
deleted file mode 100644
index e7b64c13..00000000
--- a/config/muxer.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package config
-
-import (
-	"fmt"
-
-	"github.com/libp2p/go-libp2p/core/host"
-	"github.com/libp2p/go-libp2p/core/network"
-	msmux "github.com/libp2p/go-libp2p/p2p/muxer/muxer-multistream"
-)
-
-// MuxC is a stream multiplex transport constructor.
-type MuxC func(h host.Host) (network.Multiplexer, error)
-
-// MsMuxC is a tuple containing a multiplex transport constructor and a protocol
-// ID.
-type MsMuxC struct {
-	MuxC
-	ID string
-}
-
-var muxArgTypes = newArgTypeSet(hostType, networkType, peerIDType, pstoreType)
-
-// MuxerConstructor creates a multiplex constructor from the passed parameter
-// using reflection.
-func MuxerConstructor(m interface{}) (MuxC, error) {
-	// Already constructed?
-	if t, ok := m.(network.Multiplexer); ok {
-		return func(_ host.Host) (network.Multiplexer, error) {
-			return t, nil
-		}, nil
-	}
-
-	ctor, err := makeConstructor(m, muxType, muxArgTypes)
-	if err != nil {
-		return nil, err
-	}
-	return func(h host.Host) (network.Multiplexer, error) {
-		t, err := ctor(h, nil, nil, nil, nil, nil, nil)
-		if err != nil {
-			return nil, err
-		}
-		return t.(network.Multiplexer), nil
-	}, nil
-}
-
-func makeMuxer(h host.Host, tpts []MsMuxC) (network.Multiplexer, error) {
-	muxMuxer := msmux.NewBlankTransport()
-	transportSet := make(map[string]struct{}, len(tpts))
-	for _, tptC := range tpts {
-		if _, ok := transportSet[tptC.ID]; ok {
-			return nil, fmt.Errorf("duplicate muxer transport: %s", tptC.ID)
-		}
-		transportSet[tptC.ID] = struct{}{}
-	}
-	for _, tptC := range tpts {
-		tpt, err := tptC.MuxC(h)
-		if err != nil {
-			return nil, err
-		}
-		muxMuxer.AddTransport(tptC.ID, tpt)
-	}
-	return muxMuxer, nil
-}
diff --git a/config/reflection_magic.go b/config/reflection_magic.go
deleted file mode 100644
index 0189872a..00000000
--- a/config/reflection_magic.go
+++ /dev/null
@@ -1,176 +0,0 @@
-package config
-
-import (
-	"errors"
-	"fmt"
-	"reflect"
-	"runtime"
-
-	"github.com/libp2p/go-libp2p/core/connmgr"
-	"github.com/libp2p/go-libp2p/core/host"
-	"github.com/libp2p/go-libp2p/core/network"
-	"github.com/libp2p/go-libp2p/core/pnet"
-	"github.com/libp2p/go-libp2p/core/protocol"
-	"github.com/libp2p/go-libp2p/core/transport"
-
-	madns "github.com/multiformats/go-multiaddr-dns"
-)
-
-var errorType = reflect.TypeOf((*error)(nil)).Elem()
-
-// checks if a function returns either the specified type or the specified type
-// and an error.
-func checkReturnType(fnType, tptType reflect.Type) error {
-	switch fnType.NumOut() {
-	case 2:
-		if fnType.Out(1) != errorType {
-			return fmt.Errorf("expected (optional) second return value from transport constructor to be an error")
-		}
-
-		fallthrough
-	case 1:
-		if !fnType.Out(0).Implements(tptType) {
-			return fmt.Errorf("transport constructor returns %s which doesn't implement %s", fnType.Out(0), tptType)
-		}
-	default:
-		return fmt.Errorf("expected transport constructor to return a transport and, optionally, an error")
-	}
-	return nil
-}
-
-// Handles return values with optional errors. That is, return values of the
-// form `(something, error)` or just `something`.
-//
-// Panics if the return value isn't of the correct form.
-func handleReturnValue(out []reflect.Value) (interface{}, error) {
-	switch len(out) {
-	case 2:
-		err := out[1]
-		if err != (reflect.Value{}) && !err.IsNil() {
-			return nil, err.Interface().(error)
-		}
-		fallthrough
-	case 1:
-		tpt := out[0]
-
-		// Check for nil value and nil error.
-		if tpt == (reflect.Value{}) {
-			return nil, fmt.Errorf("unspecified error")
-		}
-		switch tpt.Kind() {
-		case reflect.Ptr, reflect.Interface, reflect.Func:
-			if tpt.IsNil() {
-				return nil, fmt.Errorf("unspecified error")
-			}
-		}
-
-		return tpt.Interface(), nil
-	default:
-		panic("expected 1 or 2 return values from transport constructor")
-	}
-}
-
-// calls the transport constructor and annotates the error with the name of the constructor.
-func callConstructor(c reflect.Value, args []reflect.Value) (interface{}, error) {
-	val, err := handleReturnValue(c.Call(args))
-	if err != nil {
-		name := runtime.FuncForPC(c.Pointer()).Name()
-		if name != "" {
-			// makes debugging easier
-			return nil, fmt.Errorf("transport constructor %s failed: %s", name, err)
-		}
-	}
-	return val, err
-}
-
-type constructor func(host.Host, transport.Upgrader, pnet.PSK, connmgr.ConnectionGater, network.ResourceManager, *madns.Resolver, []protocol.ID) interface{}
-
-func makeArgumentConstructors(fnType reflect.Type, argTypes map[reflect.Type]constructor) ([]constructor, error) {
-	params := fnType.NumIn()
-	if fnType.IsVariadic() {
-		params--
-	}
-	out := make([]constructor, params)
-	for i := range out {
-		argType := fnType.In(i)
-		c, ok := argTypes[argType]
-		if !ok {
-			return nil, fmt.Errorf("argument %d has an unexpected type %s", i, argType.Name())
-		}
-		out[i] = c
-	}
-	return out, nil
-}
-
-func getConstructorOpts(t reflect.Type, opts ...interface{}) ([]reflect.Value, error) {
-	if !t.IsVariadic() {
-		if len(opts) > 0 {
-			return nil, errors.New("constructor doesn't accept any options")
-		}
-		return nil, nil
-	}
-	if len(opts) == 0 {
-		return nil, nil
-	}
-	// variadic parameters always go last
-	wantType := t.In(t.NumIn() - 1).Elem()
-	values := make([]reflect.Value, 0, len(opts))
-	for _, opt := range opts {
-		val := reflect.ValueOf(opt)
-		if opt == nil {
-			return nil, errors.New("expected a transport option, got nil")
-		}
-		if val.Type() != wantType {
-			return nil, fmt.Errorf("expected option of type %s, got %s", wantType, reflect.TypeOf(opt))
-		}
-		values = append(values, val.Convert(wantType))
-	}
-	return values, nil
-}
-
-// makes a transport constructor.
-func makeConstructor(
-	tpt interface{},
-	tptType reflect.Type,
-	argTypes map[reflect.Type]constructor,
-	opts ...interface{},
-) (func(host.Host, transport.Upgrader, pnet.PSK, connmgr.ConnectionGater, network.ResourceManager, *madns.Resolver, []protocol.ID) (interface{}, error), error) {
-	v := reflect.ValueOf(tpt)
-	// avoid panicing on nil/zero value.
-	if v == (reflect.Value{}) {
-		return nil, fmt.Errorf("expected a transport or transport constructor, got a %T", tpt)
-	}
-	t := v.Type()
-	if t.Kind() != reflect.Func {
-		return nil, fmt.Errorf("expected a transport or transport constructor, got a %T", tpt)
-	}
-
-	if err := checkReturnType(t, tptType); err != nil {
-		return nil, err
-	}
-
-	argConstructors, err := makeArgumentConstructors(t, argTypes)
-	if err != nil {
-		return nil, err
-	}
-	optValues, err := getConstructorOpts(t, opts...)
-	if err != nil {
-		return nil, err
-	}
-
-	return func(h host.Host, u transport.Upgrader, psk pnet.PSK, cg connmgr.ConnectionGater, rcmgr network.ResourceManager, resolver *madns.Resolver, muxers []protocol.ID) (interface{}, error) {
-		arguments := make([]reflect.Value, 0, len(argConstructors)+len(opts))
-		for i, makeArg := range argConstructors {
-			if arg := makeArg(h, u, psk, cg, rcmgr, resolver, muxers); arg != nil {
-				arguments = append(arguments, reflect.ValueOf(arg))
-			} else {
-				// ValueOf an un-typed nil yields a zero reflect
-				// value. However, we _want_ the zero value of
-				// the _type_.
-				arguments = append(arguments, reflect.Zero(t.In(i)))
-			}
-		}
-		arguments = append(arguments, optValues...)
-		return callConstructor(v, arguments)
-	}, nil
-}
diff --git a/config/security.go b/config/security.go
index 6f15b634..1a88575f 100644
--- a/config/security.go
+++ b/config/security.go
@@ -1,84 +1,23 @@
 package config
 
 import (
-	"fmt"
-
 	"github.com/libp2p/go-libp2p/core/crypto"
-	"github.com/libp2p/go-libp2p/core/host"
 	"github.com/libp2p/go-libp2p/core/peer"
-	"github.com/libp2p/go-libp2p/core/protocol"
 	"github.com/libp2p/go-libp2p/core/sec"
 	"github.com/libp2p/go-libp2p/core/sec/insecure"
 	csms "github.com/libp2p/go-libp2p/p2p/net/conn-security-multistream"
 )
 
-// SecC is a security transport constructor.
-type SecC func(h host.Host, muxers []protocol.ID) (sec.SecureTransport, error)
-
-// MsSecC is a tuple containing a security transport constructor and a protocol
-// ID.
-type MsSecC struct {
-	SecC
-	ID string
-}
-
-var securityArgTypes = newArgTypeSet(
-	hostType, networkType, peerIDType,
-	privKeyType, pubKeyType, pstoreType,
-	muxersType,
-)
-
-// SecurityConstructor creates a security constructor from the passed parameter
-// using reflection.
-func SecurityConstructor(security interface{}) (SecC, error) {
-	// Already constructed?
-	if t, ok := security.(sec.SecureTransport); ok {
-		return func(_ host.Host, _ []protocol.ID) (sec.SecureTransport, error) {
-			return t, nil
-		}, nil
-	}
-
-	ctor, err := makeConstructor(security, securityType, securityArgTypes)
-	if err != nil {
-		return nil, err
-	}
-	return func(h host.Host, muxers []protocol.ID) (sec.SecureTransport, error) {
-		t, err := ctor(h, nil, nil, nil, nil, nil, muxers)
-		if err != nil {
-			return nil, err
-		}
-		return t.(sec.SecureTransport), nil
-	}, nil
-}
-
 func makeInsecureTransport(id peer.ID, privKey crypto.PrivKey) sec.SecureMuxer {
 	secMuxer := new(csms.SSMuxer)
-	secMuxer.AddTransport(insecure.ID, insecure.NewWithIdentity(id, privKey))
+	secMuxer.AddTransport(insecure.ID, insecure.NewWithIdentity(insecure.ID, id, privKey))
 	return secMuxer
 }
 
-func makeSecurityMuxer(h host.Host, tpts []MsSecC, muxers []MsMuxC) (sec.SecureMuxer, error) {
+func makeSecurityMuxer(tpts []sec.SecureTransport) sec.SecureMuxer {
 	secMuxer := new(csms.SSMuxer)
-	transportSet := make(map[string]struct{}, len(tpts))
-	for _, tptC := range tpts {
-		if _, ok := transportSet[tptC.ID]; ok {
-			return nil, fmt.Errorf("duplicate security transport: %s", tptC.ID)
-		}
-		transportSet[tptC.ID] = struct{}{}
+	for _, tpt := range tpts {
+		secMuxer.AddTransport(string(tpt.ID()), tpt)
 	}
-	muxIds := make([]protocol.ID, 0, len(muxers))
-	for _, muxc := range muxers {
-		muxIds = append(muxIds, protocol.ID(muxc.ID))
-	}
-	for _, tptC := range tpts {
-		tpt, err := tptC.SecC(h, muxIds)
-		if err != nil {
-			return nil, err
-		}
-		if _, ok := tpt.(*insecure.Transport); ok {
-			return nil, fmt.Errorf("cannot construct libp2p with an insecure transport, set the Insecure config option instead")
-		}
-		secMuxer.AddTransport(tptC.ID, tpt)
-	}
-	return secMuxer, nil
+	return secMuxer
 }
diff --git a/config/transport.go b/config/transport.go
deleted file mode 100644
index 6105e77f..00000000
--- a/config/transport.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package config
-
-import (
-	"github.com/libp2p/go-libp2p/core/connmgr"
-	"github.com/libp2p/go-libp2p/core/host"
-	"github.com/libp2p/go-libp2p/core/network"
-	"github.com/libp2p/go-libp2p/core/pnet"
-	"github.com/libp2p/go-libp2p/core/transport"
-
-	madns "github.com/multiformats/go-multiaddr-dns"
-)
-
-// TptC is the type for libp2p transport constructors. You probably won't ever
-// implement this function interface directly. Instead, pass your transport
-// constructor to TransportConstructor.
-type TptC func(host.Host, transport.Upgrader, pnet.PSK, connmgr.ConnectionGater, network.ResourceManager, *madns.Resolver) (transport.Transport, error)
-
-var transportArgTypes = argTypes
-
-// TransportConstructor uses reflection to turn a function that constructs a
-// transport into a TptC.
-//
-// You can pass either a constructed transport (something that implements
-// `transport.Transport`) or a function that takes any of:
-//
-// * The local peer ID.
-// * A transport connection upgrader.
-// * A private key.
-// * A public key.
-// * A Host.
-// * A Network.
-// * A Peerstore.
-// * An address filter.
-// * A security transport.
-// * A stream multiplexer transport.
-// * A private network protection key.
-// * A connection gater.
-//
-// And returns a type implementing transport.Transport and, optionally, an error
-// (as the second argument).
-func TransportConstructor(tpt interface{}, opts ...interface{}) (TptC, error) {
-	// Already constructed?
-	if t, ok := tpt.(transport.Transport); ok {
-		return func(_ host.Host, _ transport.Upgrader, _ pnet.PSK, _ connmgr.ConnectionGater, _ network.ResourceManager, _ *madns.Resolver) (transport.Transport, error) {
-			return t, nil
-		}, nil
-	}
-	ctor, err := makeConstructor(tpt, transportType, transportArgTypes, opts...)
-	if err != nil {
-		return nil, err
-	}
-	return func(h host.Host, u transport.Upgrader, psk pnet.PSK, cg connmgr.ConnectionGater, rcmgr network.ResourceManager, resolver *madns.Resolver) (transport.Transport, error) {
-		t, err := ctor(h, u, psk, cg, rcmgr, resolver, nil)
-		if err != nil {
-			return nil, err
-		}
-		return t.(transport.Transport), nil
-	}, nil
-}
-
-func makeTransports(h host.Host, u transport.Upgrader, cg connmgr.ConnectionGater, psk pnet.PSK, rcmgr network.ResourceManager, resolver *madns.Resolver, tpts []TptC) ([]transport.Transport, error) {
-	transports := make([]transport.Transport, len(tpts))
-	for i, tC := range tpts {
-		t, err := tC(h, u, psk, cg, rcmgr, resolver)
-		if err != nil {
-			return nil, err
-		}
-		transports[i] = t
-	}
-	return transports, nil
-}
diff --git a/core/network/network.go b/core/network/network.go
index bf96697d..0beaac0f 100644
--- a/core/network/network.go
+++ b/core/network/network.go
@@ -174,10 +174,10 @@ type Dialer interface {
 	// Peers returns the peers connected
 	Peers() []peer.ID
 
-	// Conns returns the connections in this Netowrk
+	// Conns returns the connections in this Network
 	Conns() []Conn
 
-	// ConnsToPeer returns the connections in this Netowrk for given peer.
+	// ConnsToPeer returns the connections in this Network for given peer.
 	ConnsToPeer(p peer.ID) []Conn
 
 	// Notify/StopNotify register and unregister a notifiee for signals
diff --git a/core/peer/addrinfo.go b/core/peer/addrinfo.go
index 19b07a4b..b479df9e 100644
--- a/core/peer/addrinfo.go
+++ b/core/peer/addrinfo.go
@@ -86,7 +86,6 @@ func AddrInfoFromP2pAddr(m ma.Multiaddr) (*AddrInfo, error) {
 
 // AddrInfoToP2pAddrs converts an AddrInfo to a list of Multiaddrs.
 func AddrInfoToP2pAddrs(pi *AddrInfo) ([]ma.Multiaddr, error) {
-	var addrs []ma.Multiaddr
 	p2ppart, err := ma.NewComponent("p2p", Encode(pi.ID))
 	if err != nil {
 		return nil, err
@@ -94,6 +93,7 @@ func AddrInfoToP2pAddrs(pi *AddrInfo) ([]ma.Multiaddr, error) {
 	if len(pi.Addrs) == 0 {
 		return []ma.Multiaddr{p2ppart}, nil
 	}
+	addrs := make([]ma.Multiaddr, 0, len(pi.Addrs))
 	for _, addr := range pi.Addrs {
 		addrs = append(addrs, addr.Encapsulate(p2ppart))
 	}
diff --git a/core/peer/record.go b/core/peer/record.go
index 9f8861af..b502c8dd 100644
--- a/core/peer/record.go
+++ b/core/peer/record.go
@@ -231,7 +231,7 @@ func (r *PeerRecord) ToProtobuf() (*pb.PeerRecord, error) {
 }
 
 func addrsFromProtobuf(addrs []*pb.PeerRecord_AddressInfo) []ma.Multiaddr {
-	var out []ma.Multiaddr
+	out := make([]ma.Multiaddr, 0, len(addrs))
 	for _, addr := range addrs {
 		a, err := ma.NewMultiaddrBytes(addr.Multiaddr)
 		if err != nil {
@@ -243,7 +243,7 @@ func addrsFromProtobuf(addrs []*pb.PeerRecord_AddressInfo) []ma.Multiaddr {
 }
 
 func addrsToProtobuf(addrs []ma.Multiaddr) []*pb.PeerRecord_AddressInfo {
-	var out []*pb.PeerRecord_AddressInfo
+	out := make([]*pb.PeerRecord_AddressInfo, 0, len(addrs))
 	for _, addr := range addrs {
 		out = append(out, &pb.PeerRecord_AddressInfo{Multiaddr: addr.Bytes()})
 	}
diff --git a/core/protocol/switch.go b/core/protocol/switch.go
index e00ba1ad..f839e016 100644
--- a/core/protocol/switch.go
+++ b/core/protocol/switch.go
@@ -51,17 +51,6 @@ type Router interface {
 // Negotiator is a component capable of reaching agreement over what protocols
 // to use for inbound streams of communication.
 type Negotiator interface {
-	// NegotiateLazy will return the registered protocol handler to use
-	// for a given inbound stream, returning as soon as the protocol has been
-	// determined. Returns an error if negotiation fails.
-	//
-	// NegotiateLazy may return before all protocol negotiation responses have been
-	// written to the stream. This is in contrast to Negotiate, which will block until
-	// the Negotiator is finished with the stream.
-	//
-	// Deprecated: use Negotiate instead.
-	NegotiateLazy(rwc io.ReadWriteCloser) (io.ReadWriteCloser, string, HandlerFunc, error)
-
 	// Negotiate will return the registered protocol handler to use for a given
 	// inbound stream, returning after the protocol has been determined and the
 	// Negotiator has finished using the stream for negotiation. Returns an
diff --git a/core/sec/insecure/insecure.go b/core/sec/insecure/insecure.go
index 2d94f438..d2487a3b 100644
--- a/core/sec/insecure/insecure.go
+++ b/core/sec/insecure/insecure.go
@@ -12,6 +12,7 @@ import (
 	ci "github.com/libp2p/go-libp2p/core/crypto"
 	"github.com/libp2p/go-libp2p/core/network"
 	"github.com/libp2p/go-libp2p/core/peer"
+	"github.com/libp2p/go-libp2p/core/protocol"
 	"github.com/libp2p/go-libp2p/core/sec"
 	pb "github.com/libp2p/go-libp2p/core/sec/insecure/pb"
 
@@ -28,18 +29,22 @@ const ID = "/plaintext/2.0.0"
 // peer presents as their ID and public key.
 // No authentication of the remote identity is performed.
 type Transport struct {
-	id  peer.ID
-	key ci.PrivKey
+	id         peer.ID
+	key        ci.PrivKey
+	protocolID protocol.ID
 }
 
+var _ sec.SecureTransport = &Transport{}
+
 // NewWithIdentity constructs a new insecure transport. The provided private key
 // is stored and returned from LocalPrivateKey to satisfy the
 // SecureTransport interface, and the public key is sent to
 // remote peers. No security is provided.
-func NewWithIdentity(id peer.ID, key ci.PrivKey) *Transport {
+func NewWithIdentity(protocolID protocol.ID, id peer.ID, key ci.PrivKey) *Transport {
 	return &Transport{
-		id:  id,
-		key: key,
+		protocolID: protocolID,
+		id:         id,
+		key:        key,
 	}
 }
 
@@ -108,6 +113,10 @@ func (t *Transport) SecureOutbound(ctx context.Context, insecure net.Conn, p pee
 	return conn, nil
 }
 
+func (t *Transport) ID() protocol.ID {
+	return t.protocolID
+}
+
 // Conn is the connection type returned by the insecure transport.
 type Conn struct {
 	net.Conn
diff --git a/core/sec/security.go b/core/sec/security.go
index c192a56a..8b733b5d 100644
--- a/core/sec/security.go
+++ b/core/sec/security.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/libp2p/go-libp2p/core/network"
 	"github.com/libp2p/go-libp2p/core/peer"
+	"github.com/libp2p/go-libp2p/core/protocol"
 )
 
 // SecureConn is an authenticated, encrypted connection.
@@ -24,6 +25,9 @@ type SecureTransport interface {
 
 	// SecureOutbound secures an outbound connection.
 	SecureOutbound(ctx context.Context, insecure net.Conn, p peer.ID) (SecureConn, error)
+
+	// ID is the protocol ID of the security protocol.
+	ID() protocol.ID
 }
 
 // A SecureMuxer is a wrapper around SecureTransport which can select security protocols
diff --git a/error_util.go b/error_util.go
deleted file mode 100644
index 86827f4e..00000000
--- a/error_util.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package libp2p
-
-import (
-	"fmt"
-	"runtime"
-)
-
-func traceError(err error, skip int) error {
-	if err == nil {
-		return nil
-	}
-	_, file, line, ok := runtime.Caller(skip + 1)
-	if !ok {
-		return err
-	}
-	return fmt.Errorf("%s:%d: %s", file, line, err)
-}
diff --git a/go.mod b/go.mod
index 3c6128f8..365af5e1 100644
--- a/go.mod
+++ b/go.mod
@@ -31,7 +31,7 @@ require (
 	github.com/libp2p/go-reuseport v0.2.0
 	github.com/libp2p/go-yamux/v4 v4.0.0
 	github.com/libp2p/zeroconf/v2 v2.2.0
-	github.com/lucas-clemente/quic-go v0.30.0
+	github.com/lucas-clemente/quic-go v0.31.0
 	github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd
 	github.com/marten-seemann/webtransport-go v0.2.0
 	github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b
@@ -51,10 +51,11 @@ require (
 	github.com/raulk/go-watchdog v1.3.0
 	github.com/stretchr/testify v1.8.0
 	go.opencensus.io v0.23.0
+	go.uber.org/fx v1.18.2
 	go.uber.org/goleak v1.1.12
 	golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
 	golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
-	golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
+	golang.org/x/sys v0.1.1-0.20221102194838-fc697a31fa06
 )
 
 require (
@@ -105,6 +106,7 @@ require (
 	github.com/spaolacci/murmur3 v1.1.0 // indirect
 	github.com/syndtr/goleveldb v1.0.0 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
+	go.uber.org/dig v1.15.0 // indirect
 	go.uber.org/multierr v1.8.0 // indirect
 	go.uber.org/zap v1.23.0 // indirect
 	golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b // indirect
diff --git a/go.sum b/go.sum
index c9161da5..e9473541 100644
--- a/go.sum
+++ b/go.sum
@@ -320,8 +320,8 @@ github.com/libp2p/go-yamux/v4 v4.0.0 h1:+Y80dV2Yx/kv7Y7JKu0LECyVdMXm1VUoko+VQ9rB
 github.com/libp2p/go-yamux/v4 v4.0.0/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4=
 github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q=
 github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
-github.com/lucas-clemente/quic-go v0.30.0 h1:nwLW0h8ahVQ5EPTIM7uhl/stHqQDea15oRlYKZmw2O0=
-github.com/lucas-clemente/quic-go v0.30.0/go.mod h1:ssOrRsOmdxa768Wr78vnh2B8JozgLsMzG/g+0qEC7uk=
+github.com/lucas-clemente/quic-go v0.31.0 h1:MfNp3fk0wjWRajw6quMFA3ap1AVtlU+2mtwmbVogB2M=
+github.com/lucas-clemente/quic-go v0.31.0/go.mod h1:0wFbizLgYzqHqtlyxyCaJKlE7bYgE6JQ+54TLd/Dq2g=
 github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -532,6 +532,10 @@ go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
 go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/dig v1.15.0 h1:vq3YWr8zRj1eFGC7Gvf907hE0eRjPTZ1d3xHadD6liE=
+go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM=
+go.uber.org/fx v1.18.2 h1:bUNI6oShr+OVFQeU8cDNbnN7VFsu+SsjHzUF51V/GAU=
+go.uber.org/fx v1.18.2/go.mod h1:g0V1KMQ66zIRk8bLu3Ea5Jt2w/cHlOIp4wdRsgh0JaY=
 go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
 go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
 go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
@@ -727,8 +731,9 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.1-0.20221102194838-fc697a31fa06 h1:E1pm64FqQa4v8dHd/bAneyMkR4hk8LTJhoSlc5mc1cM=
+golang.org/x/sys v0.1.1-0.20221102194838-fc697a31fa06/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/options.go b/options.go
index 3509c61d..0ff63b5f 100644
--- a/options.go
+++ b/options.go
@@ -4,8 +4,11 @@ package libp2p
 // those are in defaults.go).
 
 import (
+	"crypto/rand"
+	"encoding/binary"
 	"errors"
 	"fmt"
+	"reflect"
 	"time"
 
 	"github.com/libp2p/go-libp2p/config"
@@ -13,16 +16,20 @@ import (
 	"github.com/libp2p/go-libp2p/core/crypto"
 	"github.com/libp2p/go-libp2p/core/metrics"
 	"github.com/libp2p/go-libp2p/core/network"
-	"github.com/libp2p/go-libp2p/core/peer"
 	"github.com/libp2p/go-libp2p/core/peerstore"
 	"github.com/libp2p/go-libp2p/core/pnet"
+	"github.com/libp2p/go-libp2p/core/protocol"
+	"github.com/libp2p/go-libp2p/core/sec"
+	"github.com/libp2p/go-libp2p/core/transport"
 	"github.com/libp2p/go-libp2p/p2p/host/autorelay"
 	bhost "github.com/libp2p/go-libp2p/p2p/host/basic"
+	tptu "github.com/libp2p/go-libp2p/p2p/net/upgrader"
 	relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
 	"github.com/libp2p/go-libp2p/p2p/protocol/holepunch"
 
 	ma "github.com/multiformats/go-multiaddr"
 	madns "github.com/multiformats/go-multiaddr-dns"
+	"go.uber.org/fx"
 )
 
 // ListenAddrStrings configures libp2p to listen on the given (unparsed)
@@ -61,17 +68,27 @@ func ListenAddrs(addrs ...ma.Multiaddr) Option {
 // * Host
 // * Network
 // * Peerstore
-func Security(name string, tpt interface{}) Option {
-	stpt, err := config.SecurityConstructor(tpt)
-	err = traceError(err, 1)
+func Security(name string, constructor interface{}) Option {
 	return func(cfg *Config) error {
-		if err != nil {
-			return err
-		}
 		if cfg.Insecure {
 			return fmt.Errorf("cannot use security transports with an insecure libp2p configuration")
 		}
-		cfg.SecurityTransports = append(cfg.SecurityTransports, config.MsSecC{SecC: stpt, ID: name})
+		fxName := fmt.Sprintf(`name:"%s"`, name)
+		// provide the name of the security transport
+		cfg.SecurityTransports = append(cfg.SecurityTransports,
+			fx.Provide(fx.Annotate(
+				func() protocol.ID { return protocol.ID(name) },
+				fx.ResultTags(fxName),
+			)),
+		)
+		cfg.SecurityTransports = append(cfg.SecurityTransports,
+			fx.Provide(fx.Annotate(
+				constructor,
+				fx.ParamTags(fxName),
+				fx.As(new(sec.SecureTransport)),
+				fx.ResultTags(`group:"security"`),
+			)),
+		)
 		return nil
 	}
 }
@@ -86,25 +103,11 @@ var NoSecurity Option = func(cfg *Config) error {
 	return nil
 }
 
-// Muxer configures libp2p to use the given stream multiplexer (or stream
-// multiplexer constructor).
-//
-// Name is the protocol name.
-//
-// The transport can be a constructed mux.Transport or a function taking any
-// subset of this libp2p node's:
-// * Peer ID
-// * Host
-// * Network
-// * Peerstore
-func Muxer(name string, tpt interface{}) Option {
-	mtpt, err := config.MuxerConstructor(tpt)
-	err = traceError(err, 1)
+// Muxer configures libp2p to use the given stream multiplexer.
+// name is the protocol name.
+func Muxer(name string, muxer network.Multiplexer) Option {
 	return func(cfg *Config) error {
-		if err != nil {
-			return err
-		}
-		cfg.Muxers = append(cfg.Muxers, config.MsMuxC{MuxC: mtpt, ID: name})
+		cfg.Muxers = append(cfg.Muxers, tptu.StreamMuxer{Muxer: muxer, ID: protocol.ID(name)})
 		return nil
 	}
 }
@@ -124,14 +127,55 @@ func Muxer(name string, tpt interface{}) Option {
 // * Public Key
 // * Address filter (filter.Filter)
 // * Peerstore
-func Transport(tpt interface{}, opts ...interface{}) Option {
-	tptc, err := config.TransportConstructor(tpt, opts...)
-	err = traceError(err, 1)
+func Transport(constructor interface{}, opts ...interface{}) Option {
 	return func(cfg *Config) error {
-		if err != nil {
-			return err
+		// generate a random identifier, so that fx can associate the constructor with its options
+		b := make([]byte, 8)
+		rand.Read(b)
+		id := binary.BigEndian.Uint64(b)
+
+		tag := fmt.Sprintf(`group:"transportopt_%d"`, id)
+
+		typ := reflect.ValueOf(constructor).Type()
+		numParams := typ.NumIn()
+		isVariadic := typ.IsVariadic()
+
+		if !isVariadic && len(opts) > 0 {
+			return errors.New("transport constructor doesn't take any options")
+		}
+		if isVariadic && numParams >= 1 {
+			paramType := typ.In(numParams - 1).Elem()
+			for _, opt := range opts {
+				if typ := reflect.TypeOf(opt); !typ.AssignableTo(paramType) {
+					return fmt.Errorf("transport option of type %s not assignable to %s", typ, paramType)
+				}
+			}
+		}
+
+		var params []string
+		if isVariadic && len(opts) > 0 {
+			// If there are transport options, apply the tag.
+			// Since options are variadic, they have to be the last argument of the constructor.
+			params = make([]string, numParams)
+			params[len(params)-1] = tag
+		}
+
+		cfg.Transports = append(cfg.Transports, fx.Provide(
+			fx.Annotate(
+				constructor,
+				fx.ParamTags(params...),
+				fx.As(new(transport.Transport)),
+				fx.ResultTags(`group:"transport"`),
+			),
+		))
+		for _, opt := range opts {
+			cfg.Transports = append(cfg.Transports, fx.Supply(
+				fx.Annotate(
+					opt,
+					fx.ResultTags(tag),
+				),
+			))
 		}
-		cfg.Transports = append(cfg.Transports, tptc)
 		return nil
 	}
 }
@@ -245,7 +289,9 @@ func EnableRelayService(opts ...relayv2.Option) Option {
 //
 // Dependencies:
 //   - Relay (enabled by default)
-//   - Routing (to find relays), or StaticRelays/DefaultStaticRelays.
+//   - Either:
+//     1. A list of static relays
+//     2. A PeerSource function that provides a chan of relays. See `autorelay.WithPeerSource`
 //
 // This subsystem performs automatic address rewriting to advertise relay addresses when it
 // detects that the node is publicly unreachable (e.g. behind a NAT).
@@ -257,31 +303,6 @@ func EnableAutoRelay(opts ...autorelay.Option) Option {
 	}
 }
 
-// StaticRelays configures known relays for autorelay; when this option is enabled
-// then the system will use the configured relays instead of querying the DHT to
-// discover relays.
-// Deprecated: pass an autorelay.WithStaticRelays option to EnableAutoRelay.
-func StaticRelays(relays []peer.AddrInfo) Option {
-	return func(cfg *Config) error {
-		cfg.AutoRelayOpts = append(cfg.AutoRelayOpts, autorelay.WithStaticRelays(relays))
-		return nil
-	}
-}
-
-// DefaultStaticRelays configures the static relays to use the known PL-operated relays.
-// Deprecated: pass autorelay.WithDefaultStaticRelays to EnableAutoRelay.
-func DefaultStaticRelays() Option {
-	relays := make([]peer.AddrInfo, 0, len(autorelay.DefaultRelays))
-	for _, addr := range autorelay.DefaultRelays {
-		pi, err := peer.AddrInfoFromString(addr)
-		if err != nil {
-			panic(fmt.Sprintf("failed to initialize default static relays: %s", err))
-		}
-		relays = append(relays, *pi)
-	}
-	return StaticRelays(relays)
-}
-
 // ForceReachabilityPublic overrides automatic reachability detection in the AutoNAT subsystem,
 // forcing the local node to believe it is reachable externally.
 func ForceReachabilityPublic() Option {
@@ -410,7 +431,7 @@ var NoListenAddrs = func(cfg *Config) error {
 // This will both clear any configured transports (specified in prior libp2p
 // options) and prevent libp2p from applying the default transports.
 var NoTransports = func(cfg *Config) error {
-	cfg.Transports = []config.TptC{}
+	cfg.Transports = []fx.Option{}
 	return nil
 }
 
diff --git a/p2p/discovery/util/util.go b/p2p/discovery/util/util.go
index 74175261..b21a676a 100644
--- a/p2p/discovery/util/util.go
+++ b/p2p/discovery/util/util.go
@@ -14,13 +14,13 @@ var log = logging.Logger("discovery-util")
 
 // FindPeers is a utility function that synchronously collects peers from a Discoverer.
 func FindPeers(ctx context.Context, d discovery.Discoverer, ns string, opts ...discovery.Option) ([]peer.AddrInfo, error) {
-	var res []peer.AddrInfo
 
 	ch, err := d.FindPeers(ctx, ns, opts...)
 	if err != nil {
 		return nil, err
 	}
 
+	res := make([]peer.AddrInfo, 0, len(ch))
 	for pi := range ch {
 		res = append(res, pi)
 	}
diff --git a/p2p/host/autorelay/options.go b/p2p/host/autorelay/options.go
index a6e9f595..2cfaf0b5 100644
--- a/p2p/host/autorelay/options.go
+++ b/p2p/host/autorelay/options.go
@@ -3,7 +3,6 @@ package autorelay
 import (
 	"context"
 	"errors"
-	"fmt"
 	"time"
 
 	"github.com/libp2p/go-libp2p/core/peer"
@@ -49,28 +48,6 @@ var (
 	errStaticRelaysPeerSource    = errors.New("cannot use WithPeerSource and WithStaticRelays")
 )
 
-// DefaultRelays are the known PL-operated v1 relays; will be decommissioned in 2022.
-var DefaultRelays = []string{
-	"/ip4/147.75.80.110/tcp/4001/p2p/QmbFgm5zan8P6eWWmeyfncR5feYEMPbht5b1FW1C37aQ7y",
-	"/ip4/147.75.80.110/udp/4001/quic/p2p/QmbFgm5zan8P6eWWmeyfncR5feYEMPbht5b1FW1C37aQ7y",
-	"/ip4/147.75.195.153/tcp/4001/p2p/QmW9m57aiBDHAkKj9nmFSEn7ZqrcF1fZS4bipsTCHburei",
-	"/ip4/147.75.195.153/udp/4001/quic/p2p/QmW9m57aiBDHAkKj9nmFSEn7ZqrcF1fZS4bipsTCHburei",
-	"/ip4/147.75.70.221/tcp/4001/p2p/Qme8g49gm3q4Acp7xWBKg3nAa9fxZ1YmyDJdyGgoG6LsXh",
-	"/ip4/147.75.70.221/udp/4001/quic/p2p/Qme8g49gm3q4Acp7xWBKg3nAa9fxZ1YmyDJdyGgoG6LsXh",
-}
-
-var defaultStaticRelays []peer.AddrInfo
-
-func init() {
-	for _, s := range DefaultRelays {
-		pi, err := peer.AddrInfoFromString(s)
-		if err != nil {
-			panic(fmt.Sprintf("failed to initialize default static relays: %s", err))
-		}
-		defaultStaticRelays = append(defaultStaticRelays, *pi)
-	}
-}
-
 type Option func(*config) error
 
 func WithStaticRelays(static []peer.AddrInfo) Option {
@@ -90,10 +67,6 @@ func WithStaticRelays(static []peer.AddrInfo) Option {
 	}
 }
 
-func WithDefaultStaticRelays() Option {
-	return WithStaticRelays(defaultStaticRelays)
-}
-
 // WithPeerSource defines a callback for AutoRelay to query for more relay candidates.
 // AutoRelay will call this function when it needs new candidates is connected to the desired number of
 // relays, and it has enough candidates (in case we get disconnected from one of the relays).
diff --git a/p2p/host/autorelay/relay_finder.go b/p2p/host/autorelay/relay_finder.go
index 4760511b..533c748b 100644
--- a/p2p/host/autorelay/relay_finder.go
+++ b/p2p/host/autorelay/relay_finder.go
@@ -83,6 +83,10 @@ type relayFinder struct {
 }
 
 func newRelayFinder(host *basic.BasicHost, peerSource func(context.Context, int) <-chan peer.AddrInfo, conf *config) *relayFinder {
+	if peerSource == nil && len(conf.staticRelays) == 0 {
+		panic("Can not create a new relayFinder. Need a Peer Source fn or a list of static relays. Refer to the documentation around `libp2p.EnableAutoRelay`")
+	}
+
 	return &relayFinder{
 		bootTime:                   conf.clock.Now(),
 		host:                       host,
diff --git a/p2p/host/resource-manager/limit_defaults.go b/p2p/host/resource-manager/limit_defaults.go
index dde3e7b7..27e2b8c9 100644
--- a/p2p/host/resource-manager/limit_defaults.go
+++ b/p2p/host/resource-manager/limit_defaults.go
@@ -160,6 +160,7 @@ func (cfg *LimitConfig) Apply(c LimitConfig) {
 	cfg.AllowlistedSystem.Apply(c.AllowlistedSystem)
 	cfg.AllowlistedTransient.Apply(c.AllowlistedTransient)
 	cfg.ServiceDefault.Apply(c.ServiceDefault)
+	cfg.ServicePeerDefault.Apply(c.ServicePeerDefault)
 	cfg.ProtocolDefault.Apply(c.ProtocolDefault)
 	cfg.ProtocolPeerDefault.Apply(c.ProtocolPeerDefault)
 	cfg.PeerDefault.Apply(c.PeerDefault)
diff --git a/p2p/muxer/muxer-multistream/multistream.go b/p2p/muxer/muxer-multistream/multistream.go
deleted file mode 100644
index e81ae0de..00000000
--- a/p2p/muxer/muxer-multistream/multistream.go
+++ /dev/null
@@ -1,80 +0,0 @@
-// Package muxer_multistream implements a peerstream transport using
-// go-multistream to select the underlying stream muxer
-package muxer_multistream
-
-import (
-	"fmt"
-	"net"
-	"time"
-
-	"github.com/libp2p/go-libp2p/core/network"
-
-	mss "github.com/multiformats/go-multistream"
-)
-
-var DefaultNegotiateTimeout = time.Second * 60
-
-type Transport struct {
-	mux *mss.MultistreamMuxer
-
-	tpts map[string]network.Multiplexer
-
-	NegotiateTimeout time.Duration
-
-	OrderPreference []string
-}
-
-func NewBlankTransport() *Transport {
-	return &Transport{
-		mux:              mss.NewMultistreamMuxer(),
-		tpts:             make(map[string]network.Multiplexer),
-		NegotiateTimeout: DefaultNegotiateTimeout,
-	}
-}
-
-func (t *Transport) AddTransport(path string, tpt network.Multiplexer) {
-	t.mux.AddHandler(path, nil)
-	t.tpts[path] = tpt
-	t.OrderPreference = append(t.OrderPreference, path)
-}
-
-func (t *Transport) NewConn(nc net.Conn, isServer bool, scope network.PeerScope) (network.MuxedConn, error) {
-	if t.NegotiateTimeout != 0 {
-		if err := nc.SetDeadline(time.Now().Add(t.NegotiateTimeout)); err != nil {
-			return nil, err
-		}
-	}
-
-	var proto string
-	if isServer {
-		selected, _, err := t.mux.Negotiate(nc)
-		if err != nil {
-			return nil, err
-		}
-		proto = selected
-	} else {
-		selected, err := mss.SelectOneOf(t.OrderPreference, nc)
-		if err != nil {
-			return nil, err
-		}
-		proto = selected
-	}
-
-	if t.NegotiateTimeout != 0 {
-		if err := nc.SetDeadline(time.Time{}); err != nil {
-			return nil, err
-		}
-	}
-
-	tpt, ok := t.tpts[proto]
-	if !ok {
-		return nil, fmt.Errorf("selected protocol we don't have a transport for")
-	}
-
-	return tpt.NewConn(nc, isServer, scope)
-}
-
-func (t *Transport) GetTransportByKey(key string) (network.Multiplexer, bool) {
-	val, ok := t.tpts[key]
-	return val, ok
-}
diff --git a/p2p/net/connmgr/connmgr.go b/p2p/net/connmgr/connmgr.go
index 3a1acf0d..c124c6bb 100644
--- a/p2p/net/connmgr/connmgr.go
+++ b/p2p/net/connmgr/connmgr.go
@@ -62,14 +62,21 @@ type segment struct {
 	peers map[peer.ID]*peerInfo
 }
 
-type segments [256]*segment
+type segments struct {
+	// bucketsMu is used to prevent deadlocks when concurrent processes try to
+	// grab multiple segment locks at once. If you need multiple segment locks
+	// at once, you should grab this lock first. You may release this lock once
+	// you have the segment locks.
+	bucketsMu sync.Mutex
+	buckets   [256]*segment
+}
 
 func (ss *segments) get(p peer.ID) *segment {
-	return ss[byte(p[len(p)-1])]
+	return ss.buckets[byte(p[len(p)-1])]
 }
 
 func (ss *segments) countPeers() (count int) {
-	for _, seg := range ss {
+	for _, seg := range ss.buckets {
 		seg.Lock()
 		count += len(seg.peers)
 		seg.Unlock()
@@ -122,15 +129,15 @@ func NewConnManager(low, hi int, opts ...Option) (*BasicConnMgr, error) {
 		cfg:       cfg,
 		clock:     cfg.clock,
 		protected: make(map[peer.ID]map[string]struct{}, 16),
-		segments: func() (ret segments) {
-			for i := range ret {
-				ret[i] = &segment{
-					peers: make(map[peer.ID]*peerInfo),
-				}
-			}
-			return ret
-		}(),
+		segments:  segments{},
+	}
+
+	for i := range cm.segments.buckets {
+		cm.segments.buckets[i] = &segment{
+			peers: make(map[peer.ID]*peerInfo),
+		}
 	}
+
 	cm.ctx, cm.cancel = context.WithCancel(context.Background())
 
 	if cfg.emergencyTrim {
@@ -246,15 +253,32 @@ type peerInfo struct {
 	firstSeen time.Time // timestamp when we began tracking this peer.
 }
 
-type peerInfos []peerInfo
+type peerInfos []*peerInfo
 
 // SortByValueAndStreams sorts peerInfos by their value and stream count. It
 // will sort peers with no streams before those with streams (all else being
 // equal). If `sortByMoreStreams` is true it will sort peers with more streams
 // before those with fewer streams. This is useful to prioritize freeing memory.
-func (p peerInfos) SortByValueAndStreams(sortByMoreStreams bool) {
+func (p peerInfos) SortByValueAndStreams(segments *segments, sortByMoreStreams bool) {
 	sort.Slice(p, func(i, j int) bool {
 		left, right := p[i], p[j]
+
+		// Grab this lock so that we can grab both segment locks below without deadlocking.
+		segments.bucketsMu.Lock()
+
+		// lock this to protect from concurrent modifications from connect/disconnect events
+		leftSegment := segments.get(left.id)
+		leftSegment.Lock()
+		defer leftSegment.Unlock()
+
+		rightSegment := segments.get(right.id)
+		if leftSegment != rightSegment {
+			// These two peers are not in the same segment, lets get the lock
+			rightSegment.Lock()
+			defer rightSegment.Unlock()
+		}
+		segments.bucketsMu.Unlock()
+
 		// temporary peers are preferred for pruning.
 		if left.temp != right.temp {
 			return left.temp
@@ -360,31 +384,34 @@ func (cm *BasicConnMgr) getConnsToCloseEmergency(target int) []network.Conn {
 	candidates := make(peerInfos, 0, cm.segments.countPeers())
 
 	cm.plk.RLock()
-	for _, s := range cm.segments {
+	for _, s := range cm.segments.buckets {
 		s.Lock()
 		for id, inf := range s.peers {
 			if _, ok := cm.protected[id]; ok {
 				// skip over protected peer.
 				continue
 			}
-			candidates = append(candidates, *inf)
+			candidates = append(candidates, inf)
 		}
 		s.Unlock()
 	}
 	cm.plk.RUnlock()
 
 	// Sort peers according to their value.
-	candidates.SortByValueAndStreams(true)
+	candidates.SortByValueAndStreams(&cm.segments, true)
 
 	selected := make([]network.Conn, 0, target+10)
 	for _, inf := range candidates {
 		if target <= 0 {
 			break
 		}
+		s := cm.segments.get(inf.id)
+		s.Lock()
 		for c := range inf.conns {
 			selected = append(selected, c)
 		}
 		target -= len(inf.conns)
+		s.Unlock()
 	}
 	if len(selected) >= target {
 		// We found enough connections that were not protected.
@@ -395,24 +422,28 @@ func (cm *BasicConnMgr) getConnsToCloseEmergency(target int) []network.Conn {
 	// We have no choice but to kill some protected connections.
 	candidates = candidates[:0]
 	cm.plk.RLock()
-	for _, s := range cm.segments {
+	for _, s := range cm.segments.buckets {
 		s.Lock()
 		for _, inf := range s.peers {
-			candidates = append(candidates, *inf)
+			candidates = append(candidates, inf)
 		}
 		s.Unlock()
 	}
 	cm.plk.RUnlock()
 
-	candidates.SortByValueAndStreams(true)
+	candidates.SortByValueAndStreams(&cm.segments, true)
 	for _, inf := range candidates {
 		if target <= 0 {
 			break
 		}
+		// lock this to protect from concurrent modifications from connect/disconnect events
+		s := cm.segments.get(inf.id)
+		s.Lock()
 		for c := range inf.conns {
 			selected = append(selected, c)
 		}
 		target -= len(inf.conns)
+		s.Unlock()
 	}
 	return selected
 }
@@ -435,7 +466,7 @@ func (cm *BasicConnMgr) getConnsToClose() []network.Conn {
 	gracePeriodStart := cm.clock.Now().Add(-cm.cfg.gracePeriod)
 
 	cm.plk.RLock()
-	for _, s := range cm.segments {
+	for _, s := range cm.segments.buckets {
 		s.Lock()
 		for id, inf := range s.peers {
 			if _, ok := cm.protected[id]; ok {
@@ -448,7 +479,7 @@ func (cm *BasicConnMgr) getConnsToClose() []network.Conn {
 			}
 			// note that we're copying the entry here,
 			// but since inf.conns is a map, it will still point to the original object
-			candidates = append(candidates, *inf)
+			candidates = append(candidates, inf)
 			ncandidates += len(inf.conns)
 		}
 		s.Unlock()
@@ -465,7 +496,7 @@ func (cm *BasicConnMgr) getConnsToClose() []network.Conn {
 	}
 
 	// Sort peers according to their value.
-	candidates.SortByValueAndStreams(false)
+	candidates.SortByValueAndStreams(&cm.segments, false)
 
 	target := ncandidates - cm.cfg.lowWater
 
diff --git a/p2p/net/connmgr/decay.go b/p2p/net/connmgr/decay.go
index 0819bd21..c10214cb 100644
--- a/p2p/net/connmgr/decay.go
+++ b/p2p/net/connmgr/decay.go
@@ -177,7 +177,7 @@ func (d *decayer) process() {
 			d.tagsMu.Unlock()
 
 			// Visit each peer, and decay tags that need to be decayed.
-			for _, s := range d.mgr.segments {
+			for _, s := range d.mgr.segments.buckets {
 				s.Lock()
 
 				// Entered a segment that contains peers. Process each peer.
@@ -261,7 +261,7 @@ func (d *decayer) process() {
 			d.tagsMu.Unlock()
 
 			// Remove the tag from all peers that had it in the connmgr.
-			for _, s := range d.mgr.segments {
+			for _, s := range d.mgr.segments.buckets {
 				// visit all segments, and attempt to remove the tag from all the peers it stores.
 				s.Lock()
 				for _, p := range s.peers {
diff --git a/p2p/net/mock/mock_peernet.go b/p2p/net/mock/mock_peernet.go
index 3fb07013..85e63678 100644
--- a/p2p/net/mock/mock_peernet.go
+++ b/p2p/net/mock/mock_peernet.go
@@ -255,7 +255,7 @@ func (pn *peernet) ConnsToPeer(p peer.ID) []network.Conn {
 		return nil
 	}
 
-	var cs2 []network.Conn
+	cs2 := make([]network.Conn, 0, len(cs))
 	for c := range cs {
 		cs2 = append(cs2, c)
 	}
diff --git a/p2p/net/swarm/swarm_dial.go b/p2p/net/swarm/swarm_dial.go
index f5e5cba8..1efde8c4 100644
--- a/p2p/net/swarm/swarm_dial.go
+++ b/p2p/net/swarm/swarm_dial.go
@@ -334,7 +334,7 @@ func (s *Swarm) addrsForDial(ctx context.Context, p peer.ID) ([]ma.Multiaddr, er
 
 	s.peers.AddAddrs(p, goodAddrs, peerstore.TempAddrTTL)
 
-	return resolved, nil
+	return goodAddrs, nil
 }
 
 func (s *Swarm) resolveAddrs(ctx context.Context, pi peer.AddrInfo) ([]ma.Multiaddr, error) {
diff --git a/p2p/net/upgrader/conn.go b/p2p/net/upgrader/conn.go
index 2fa7af64..fa736491 100644
--- a/p2p/net/upgrader/conn.go
+++ b/p2p/net/upgrader/conn.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/libp2p/go-libp2p/core/network"
+	"github.com/libp2p/go-libp2p/core/protocol"
 	"github.com/libp2p/go-libp2p/core/transport"
 )
 
@@ -14,6 +15,8 @@ type transportConn struct {
 	transport transport.Transport
 	scope     network.ConnManagementScope
 	stat      network.ConnStats
+
+	muxer protocol.ID
 }
 
 var _ transport.CapableConn = &transportConn{}
@@ -49,3 +52,7 @@ func (t *transportConn) Close() error {
 	defer t.scope.Done()
 	return t.MuxedConn.Close()
 }
+
+func (t *transportConn) ConnState() network.ConnectionState {
+	return network.ConnectionState{NextProto: string(t.muxer)}
+}
diff --git a/p2p/net/upgrader/upgrader.go b/p2p/net/upgrader/upgrader.go
index ea19d2f2..403034db 100644
--- a/p2p/net/upgrader/upgrader.go
+++ b/p2p/net/upgrader/upgrader.go
@@ -11,12 +11,13 @@ import (
 	"github.com/libp2p/go-libp2p/core/network"
 	"github.com/libp2p/go-libp2p/core/peer"
 	ipnet "github.com/libp2p/go-libp2p/core/pnet"
+	"github.com/libp2p/go-libp2p/core/protocol"
 	"github.com/libp2p/go-libp2p/core/sec"
 	"github.com/libp2p/go-libp2p/core/transport"
-	msmux "github.com/libp2p/go-libp2p/p2p/muxer/muxer-multistream"
 	"github.com/libp2p/go-libp2p/p2p/net/pnet"
 
 	manet "github.com/multiformats/go-multiaddr/net"
+	mss "github.com/multiformats/go-multistream"
 )
 
 // ErrNilPeer is returned when attempting to upgrade an outbound connection
@@ -26,17 +27,13 @@ var ErrNilPeer = errors.New("nil peer")
 // AcceptQueueLength is the number of connections to fully setup before not accepting any new connections
 var AcceptQueueLength = 16
 
-const defaultAcceptTimeout = 15 * time.Second
+const (
+	defaultAcceptTimeout    = 15 * time.Second
+	defaultNegotiateTimeout = 60 * time.Second
+)
 
 type Option func(*upgrader) error
 
-func WithPSK(psk ipnet.PSK) Option {
-	return func(u *upgrader) error {
-		u.psk = psk
-		return nil
-	}
-}
-
 func WithAcceptTimeout(t time.Duration) Option {
 	return func(u *upgrader) error {
 		u.acceptTimeout = t
@@ -44,30 +41,24 @@ func WithAcceptTimeout(t time.Duration) Option {
 	}
 }
 
-func WithConnectionGater(g connmgr.ConnectionGater) Option {
-	return func(u *upgrader) error {
-		u.connGater = g
-		return nil
-	}
-}
-
-func WithResourceManager(m network.ResourceManager) Option {
-	return func(u *upgrader) error {
-		u.rcmgr = m
-		return nil
-	}
+type StreamMuxer struct {
+	ID    protocol.ID
+	Muxer network.Multiplexer
 }
 
 // Upgrader is a multistream upgrader that can upgrade an underlying connection
 // to a full transport connection (secure and multiplexed).
 type upgrader struct {
 	secure sec.SecureMuxer
-	muxer  network.Multiplexer
 
 	psk       ipnet.PSK
 	connGater connmgr.ConnectionGater
 	rcmgr     network.ResourceManager
 
+	msmuxer  *mss.MultistreamMuxer
+	muxers   []StreamMuxer
+	muxerIDs []string
+
 	// AcceptTimeout is the maximum duration an Accept is allowed to take.
 	// This includes the time between accepting the raw network connection,
 	// protocol selection as well as the handshake, if applicable.
@@ -78,11 +69,15 @@ type upgrader struct {
 
 var _ transport.Upgrader = &upgrader{}
 
-func New(secureMuxer sec.SecureMuxer, muxer network.Multiplexer, opts ...Option) (transport.Upgrader, error) {
+func New(secureMuxer sec.SecureMuxer, muxers []StreamMuxer, psk ipnet.PSK, rcmgr network.ResourceManager, connGater connmgr.ConnectionGater, opts ...Option) (transport.Upgrader, error) {
 	u := &upgrader{
 		secure:        secureMuxer,
-		muxer:         muxer,
 		acceptTimeout: defaultAcceptTimeout,
+		rcmgr:         rcmgr,
+		connGater:     connGater,
+		psk:           psk,
+		msmuxer:       mss.NewMultistreamMuxer(),
+		muxers:        muxers,
 	}
 	for _, opt := range opts {
 		if err := opt(u); err != nil {
@@ -92,6 +87,11 @@ func New(secureMuxer sec.SecureMuxer, muxer network.Multiplexer, opts ...Option)
 	if u.rcmgr == nil {
 		u.rcmgr = &network.NullResourceManager{}
 	}
+	u.muxerIDs = make([]string, 0, len(muxers))
+	for _, m := range muxers {
+		u.msmuxer.AddHandler(string(m.ID), nil)
+		u.muxerIDs = append(u.muxerIDs, string(m.ID))
+	}
 	return u, nil
 }
 
@@ -171,7 +171,7 @@ func (u *upgrader) upgrade(ctx context.Context, t transport.Transport, maconn ma
 		}
 	}
 
-	smconn, err := u.setupMuxer(ctx, sconn, server, connScope.PeerScope())
+	muxer, smconn, err := u.setupMuxer(ctx, sconn, server, connScope.PeerScope())
 	if err != nil {
 		sconn.Close()
 		return nil, fmt.Errorf("failed to negotiate stream multiplexer: %s", err)
@@ -184,6 +184,7 @@ func (u *upgrader) upgrade(ctx context.Context, t transport.Transport, maconn ma
 		transport:      t,
 		stat:           stat,
 		scope:          connScope,
+		muxer:          muxer,
 	}
 	return tc, nil
 }
@@ -195,37 +196,86 @@ func (u *upgrader) setupSecurity(ctx context.Context, conn net.Conn, p peer.ID,
 	return u.secure.SecureOutbound(ctx, conn, p)
 }
 
-func (u *upgrader) setupMuxer(ctx context.Context, conn sec.SecureConn, server bool, scope network.PeerScope) (network.MuxedConn, error) {
-	msmuxer, ok := u.muxer.(*msmux.Transport)
+func (u *upgrader) negotiateMuxer(nc net.Conn, isServer bool) (*StreamMuxer, error) {
+	if err := nc.SetDeadline(time.Now().Add(defaultNegotiateTimeout)); err != nil {
+		return nil, err
+	}
+
+	var proto string
+	if isServer {
+		selected, _, err := u.msmuxer.Negotiate(nc)
+		if err != nil {
+			return nil, err
+		}
+		proto = selected
+	} else {
+		selected, err := mss.SelectOneOf(u.muxerIDs, nc)
+		if err != nil {
+			return nil, err
+		}
+		proto = selected
+	}
+
+	if err := nc.SetDeadline(time.Time{}); err != nil {
+		return nil, err
+	}
+
+	if m := u.getMuxerByID(proto); m != nil {
+		return m, nil
+	}
+	return nil, fmt.Errorf("selected protocol we don't have a transport for")
+}
+
+func (u *upgrader) getMuxerByID(id string) *StreamMuxer {
+	for _, m := range u.muxers {
+		if string(m.ID) == id {
+			return &m
+		}
+	}
+	return nil
+}
+
+func (u *upgrader) setupMuxer(ctx context.Context, conn sec.SecureConn, server bool, scope network.PeerScope) (protocol.ID, network.MuxedConn, error) {
 	muxerSelected := conn.ConnState().NextProto
 	// Use muxer selected from security handshake if available. Otherwise fall back to multistream-selection.
-	if ok && len(muxerSelected) > 0 {
-		tpt, ok := msmuxer.GetTransportByKey(muxerSelected)
-		if !ok {
-			return nil, fmt.Errorf("selected a muxer we don't know: %s", muxerSelected)
+	if len(muxerSelected) > 0 {
+		m := u.getMuxerByID(muxerSelected)
+		if m == nil {
+			return "", nil, fmt.Errorf("selected a muxer we don't know: %s", muxerSelected)
 		}
-
-		return tpt.NewConn(conn, server, scope)
+		c, err := m.Muxer.NewConn(conn, server, scope)
+		if err != nil {
+			return "", nil, err
+		}
+		return protocol.ID(muxerSelected), c, nil
 	}
 
-	done := make(chan struct{})
+	type result struct {
+		smconn  network.MuxedConn
+		muxerID protocol.ID
+		err     error
+	}
 
-	var smconn network.MuxedConn
-	var err error
+	done := make(chan result, 1)
 	// TODO: The muxer should take a context.
 	go func() {
-		defer close(done)
-		smconn, err = u.muxer.NewConn(conn, server, scope)
+		m, err := u.negotiateMuxer(conn, server)
+		if err != nil {
+			done <- result{err: err}
+			return
+		}
+		smconn, err := m.Muxer.NewConn(conn, server, scope)
+		done <- result{smconn: smconn, muxerID: m.ID, err: err}
 	}()
 
 	select {
-	case <-done:
-		return smconn, err
+	case r := <-done:
+		return r.muxerID, r.smconn, r.err
 	case <-ctx.Done():
 		// interrupt this process
 		conn.Close()
 		// wait to finish
 		<-done
-		return nil, ctx.Err()
+		return "", nil, ctx.Err()
 	}
 }
diff --git a/p2p/protocol/circuitv2/util/pbconv.go b/p2p/protocol/circuitv2/util/pbconv.go
index ae1b9b50..4a884351 100644
--- a/p2p/protocol/circuitv2/util/pbconv.go
+++ b/p2p/protocol/circuitv2/util/pbconv.go
@@ -36,10 +36,7 @@ func PeerToPeerInfoV1(p *pbv1.CircuitRelay_Peer) (peer.AddrInfo, error) {
 }
 
 func PeerInfoToPeerV1(pi peer.AddrInfo) *pbv1.CircuitRelay_Peer {
-	var addrs [][]byte
-	if len(pi.Addrs) > 0 {
-		addrs = make([][]byte, 0, len(pi.Addrs))
-	}
+	addrs := make([][]byte, 0, len(pi.Addrs))
 
 	for _, addr := range pi.Addrs {
 		addrs = append(addrs, addr.Bytes())
@@ -62,10 +59,7 @@ func PeerToPeerInfoV2(p *pbv2.Peer) (peer.AddrInfo, error) {
 		return peer.AddrInfo{}, err
 	}
 
-	var addrs []ma.Multiaddr
-	if len(p.Addrs) > 0 {
-		addrs = make([]ma.Multiaddr, 0, len(p.Addrs))
-	}
+	addrs := make([]ma.Multiaddr, 0, len(p.Addrs))
 
 	for _, addrBytes := range p.Addrs {
 		a, err := ma.NewMultiaddrBytes(addrBytes)
@@ -78,11 +72,7 @@ func PeerToPeerInfoV2(p *pbv2.Peer) (peer.AddrInfo, error) {
 }
 
 func PeerInfoToPeerV2(pi peer.AddrInfo) *pbv2.Peer {
-	var addrs [][]byte
-
-	if len(pi.Addrs) > 0 {
-		addrs = make([][]byte, 0, len(pi.Addrs))
-	}
+	addrs := make([][]byte, 0, len(pi.Addrs))
 
 	for _, addr := range pi.Addrs {
 		addrs = append(addrs, addr.Bytes())
diff --git a/p2p/protocol/holepunch/filter.go b/p2p/protocol/holepunch/filter.go
new file mode 100644
index 00000000..5c1a4f53
--- /dev/null
+++ b/p2p/protocol/holepunch/filter.go
@@ -0,0 +1,27 @@
+package holepunch
+
+import (
+	"github.com/libp2p/go-libp2p/core/peer"
+	ma "github.com/multiformats/go-multiaddr"
+)
+
+// WithAddrFilter is a Service option that enables multiaddress filtering.
+// It allows to only send a subset of observed addresses to the remote
+// peer. E.g., only announce TCP or QUIC multi addresses instead of both.
+// It also allows to only consider a subset of received multi addresses
+// that remote peers announced to us.
+// Theoretically, this API also allows to add multi addresses in both cases.
+func WithAddrFilter(f AddrFilter) Option {
+	return func(hps *Service) error {
+		hps.filter = f
+		return nil
+	}
+}
+
+// AddrFilter defines the interface for the multi address filtering.
+type AddrFilter interface {
+	// FilterLocal filters the multi addresses that are sent to the remote peer.
+	FilterLocal(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr
+	// FilterRemote filters the multi addresses received from the remote peer.
+	FilterRemote(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr
+}
diff --git a/p2p/protocol/holepunch/holepuncher.go b/p2p/protocol/holepunch/holepuncher.go
index 7f2bbbd1..5cf8f8fd 100644
--- a/p2p/protocol/holepunch/holepuncher.go
+++ b/p2p/protocol/holepunch/holepuncher.go
@@ -49,14 +49,16 @@ type holePuncher struct {
 	closed  bool
 
 	tracer *tracer
+	filter AddrFilter
 }
 
-func newHolePuncher(h host.Host, ids identify.IDService, tracer *tracer) *holePuncher {
+func newHolePuncher(h host.Host, ids identify.IDService, tracer *tracer, filter AddrFilter) *holePuncher {
 	hp := &holePuncher{
 		host:   h,
 		ids:    ids,
 		active: make(map[peer.ID]struct{}),
 		tracer: tracer,
+		filter: filter,
 	}
 	hp.ctx, hp.ctxCancel = context.WithCancel(context.Background())
 	h.Network().Notify((*netNotifiee)(hp))
@@ -204,10 +206,18 @@ func (hp *holePuncher) initiateHolePunchImpl(str network.Stream) ([]ma.Multiaddr
 	str.SetDeadline(time.Now().Add(StreamTimeout))
 
 	// send a CONNECT and start RTT measurement.
+	obsAddrs := removeRelayAddrs(hp.ids.OwnObservedAddrs())
+	if hp.filter != nil {
+		obsAddrs = hp.filter.FilterLocal(str.Conn().RemotePeer(), obsAddrs)
+	}
+	if len(obsAddrs) == 0 {
+		return nil, 0, errors.New("aborting hole punch initiation as we have no public address")
+	}
+
 	start := time.Now()
 	if err := w.WriteMsg(&pb.HolePunch{
 		Type:     pb.HolePunch_CONNECT.Enum(),
-		ObsAddrs: addrsToBytes(removeRelayAddrs(hp.ids.OwnObservedAddrs())),
+		ObsAddrs: addrsToBytes(obsAddrs),
 	}); err != nil {
 		str.Reset()
 		return nil, 0, err
@@ -222,7 +232,12 @@ func (hp *holePuncher) initiateHolePunchImpl(str network.Stream) ([]ma.Multiaddr
 	if t := msg.GetType(); t != pb.HolePunch_CONNECT {
 		return nil, 0, fmt.Errorf("expect CONNECT message, got %s", t)
 	}
+
 	addrs := removeRelayAddrs(addrsFromBytes(msg.ObsAddrs))
+	if hp.filter != nil {
+		addrs = hp.filter.FilterRemote(str.Conn().RemotePeer(), addrs)
+	}
+
 	if len(addrs) == 0 {
 		return nil, 0, errors.New("didn't receive any public addresses in CONNECT")
 	}
diff --git a/p2p/protocol/holepunch/svc.go b/p2p/protocol/holepunch/svc.go
index 1df779fc..2f9049c8 100644
--- a/p2p/protocol/holepunch/svc.go
+++ b/p2p/protocol/holepunch/svc.go
@@ -53,6 +53,7 @@ type Service struct {
 	hasPublicAddrsChan chan struct{}
 
 	tracer *tracer
+	filter AddrFilter
 
 	refCount sync.WaitGroup
 }
@@ -140,7 +141,7 @@ func (s *Service) watchForPublicAddr() {
 				continue
 			}
 			s.holePuncherMx.Lock()
-			s.holePuncher = newHolePuncher(s.host, s.ids, s.tracer)
+			s.holePuncher = newHolePuncher(s.host, s.ids, s.tracer, s.filter)
 			s.holePuncherMx.Unlock()
 			close(s.hasPublicAddrsChan)
 			return
@@ -169,6 +170,10 @@ func (s *Service) incomingHolePunch(str network.Stream) (rtt time.Duration, addr
 		return 0, nil, fmt.Errorf("received hole punch stream: %s", str.Conn().RemoteMultiaddr())
 	}
 	ownAddrs := removeRelayAddrs(s.ids.OwnObservedAddrs())
+	if s.filter != nil {
+		ownAddrs = s.filter.FilterLocal(str.Conn().RemotePeer(), ownAddrs)
+	}
+
 	// If we can't tell the peer where to dial us, there's no point in starting the hole punching.
 	if len(ownAddrs) == 0 {
 		return 0, nil, errors.New("rejecting hole punch request, as we don't have any public addresses")
@@ -194,7 +199,12 @@ func (s *Service) incomingHolePunch(str network.Stream) (rtt time.Duration, addr
 	if t := msg.GetType(); t != pb.HolePunch_CONNECT {
 		return 0, nil, fmt.Errorf("expected CONNECT message from initiator but got %d", t)
 	}
+
 	obsDial := removeRelayAddrs(addrsFromBytes(msg.ObsAddrs))
+	if s.filter != nil {
+		obsDial = s.filter.FilterRemote(str.Conn().RemotePeer(), obsDial)
+	}
+
 	log.Debugw("received hole punch request", "peer", str.Conn().RemotePeer(), "addrs", obsDial)
 	if len(obsDial) == 0 {
 		return 0, nil, errors.New("expected CONNECT message to contain at least one address")
diff --git a/p2p/security/noise/handshake.go b/p2p/security/noise/handshake.go
index 6e119cb5..4dd481d6 100644
--- a/p2p/security/noise/handshake.go
+++ b/p2p/security/noise/handshake.go
@@ -272,11 +272,8 @@ func (s *secureSession) handleRemoteHandshakePayload(payload []byte, remoteStati
 		return nil, err
 	}
 
-	// check the peer ID for:
-	// * all outbound connection
-	// * inbound connections, if we know which peer we want to connect to (SecureInbound called with a peer ID)
-	if (s.initiator && s.remoteID != id) || (!s.initiator && s.remoteID != "" && s.remoteID != id) {
-		// use Pretty() as it produces the full b58-encoded string, rather than abbreviated forms.
+	// check the peer ID if enabled
+	if s.checkPeerID && s.remoteID != id {
 		return nil, fmt.Errorf("peer id mismatch: expected %s, but remote key matches %s", s.remoteID.Pretty(), id.Pretty())
 	}
 
diff --git a/p2p/security/noise/session.go b/p2p/security/noise/session.go
index ce8d97cd..f7e75e64 100644
--- a/p2p/security/noise/session.go
+++ b/p2p/security/noise/session.go
@@ -15,7 +15,8 @@ import (
 )
 
 type secureSession struct {
-	initiator bool
+	initiator   bool
+	checkPeerID bool
 
 	localID   peer.ID
 	localKey  crypto.PrivKey
@@ -47,7 +48,7 @@ type secureSession struct {
 
 // newSecureSession creates a Noise session over the given insecureConn Conn, using
 // the libp2p identity keypair from the given Transport.
-func newSecureSession(tpt *Transport, ctx context.Context, insecure net.Conn, remote peer.ID, prologue []byte, initiatorEDH, responderEDH EarlyDataHandler, initiator bool) (*secureSession, error) {
+func newSecureSession(tpt *Transport, ctx context.Context, insecure net.Conn, remote peer.ID, prologue []byte, initiatorEDH, responderEDH EarlyDataHandler, initiator, checkPeerID bool) (*secureSession, error) {
 	s := &secureSession{
 		insecureConn:              insecure,
 		insecureReader:            bufio.NewReader(insecure),
@@ -58,6 +59,7 @@ func newSecureSession(tpt *Transport, ctx context.Context, insecure net.Conn, re
 		prologue:                  prologue,
 		initiatorEarlyDataHandler: initiatorEDH,
 		responderEarlyDataHandler: responderEDH,
+		checkPeerID:               checkPeerID,
 	}
 
 	// the go-routine we create to run the handshake will
diff --git a/p2p/security/noise/session_transport.go b/p2p/security/noise/session_transport.go
index 7b468edb..0f26f3fa 100644
--- a/p2p/security/noise/session_transport.go
+++ b/p2p/security/noise/session_transport.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/libp2p/go-libp2p/core/canonicallog"
 	"github.com/libp2p/go-libp2p/core/peer"
+	"github.com/libp2p/go-libp2p/core/protocol"
 	"github.com/libp2p/go-libp2p/core/sec"
 	"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
 
@@ -50,6 +51,17 @@ func EarlyData(initiator, responder EarlyDataHandler) SessionOption {
 	}
 }
 
+// DisablePeerIDCheck disables checking the remote peer ID for a noise connection.
+// For outbound connections, this is the equivalent of calling `SecureInbound` with an empty
+// peer ID. This is susceptible to MITM attacks since we do not verify the identity of the remote
+// peer.
+func DisablePeerIDCheck() SessionOption {
+	return func(s *SessionTransport) error {
+		s.disablePeerIDCheck = true
+		return nil
+	}
+}
+
 var _ sec.SecureTransport = &SessionTransport{}
 
 // SessionTransport can be used
@@ -57,7 +69,10 @@ var _ sec.SecureTransport = &SessionTransport{}
 type SessionTransport struct {
 	t *Transport
 	// options
-	prologue []byte
+	prologue           []byte
+	disablePeerIDCheck bool
+
+	protocolID protocol.ID
 
 	initiatorEarlyDataHandler, responderEarlyDataHandler EarlyDataHandler
 }
@@ -65,7 +80,8 @@ type SessionTransport struct {
 // SecureInbound runs the Noise handshake as the responder.
 // If p is empty, connections from any peer are accepted.
 func (i *SessionTransport) SecureInbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
-	c, err := newSecureSession(i.t, ctx, insecure, p, i.prologue, i.initiatorEarlyDataHandler, i.responderEarlyDataHandler, false)
+	checkPeerID := !i.disablePeerIDCheck && p != ""
+	c, err := newSecureSession(i.t, ctx, insecure, p, i.prologue, i.initiatorEarlyDataHandler, i.responderEarlyDataHandler, false, checkPeerID)
 	if err != nil {
 		addr, maErr := manet.FromNetAddr(insecure.RemoteAddr())
 		if maErr == nil {
@@ -77,5 +93,9 @@ func (i *SessionTransport) SecureInbound(ctx context.Context, insecure net.Conn,
 
 // SecureOutbound runs the Noise handshake as the initiator.
 func (i *SessionTransport) SecureOutbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
-	return newSecureSession(i.t, ctx, insecure, p, i.prologue, i.initiatorEarlyDataHandler, i.responderEarlyDataHandler, true)
+	return newSecureSession(i.t, ctx, insecure, p, i.prologue, i.initiatorEarlyDataHandler, i.responderEarlyDataHandler, true, !i.disablePeerIDCheck)
+}
+
+func (i *SessionTransport) ID() protocol.ID {
+	return i.protocolID
 }
diff --git a/p2p/security/noise/transport.go b/p2p/security/noise/transport.go
index 7ae4d7de..6e2882b8 100644
--- a/p2p/security/noise/transport.go
+++ b/p2p/security/noise/transport.go
@@ -9,41 +9,40 @@ import (
 	"github.com/libp2p/go-libp2p/core/peer"
 	"github.com/libp2p/go-libp2p/core/protocol"
 	"github.com/libp2p/go-libp2p/core/sec"
+	tptu "github.com/libp2p/go-libp2p/p2p/net/upgrader"
 	"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
 
 	manet "github.com/multiformats/go-multiaddr/net"
 )
 
 // ID is the protocol ID for noise
-const (
-	ID          = "/noise"
-	maxProtoNum = 100
-)
-
-var _ sec.SecureTransport = &Transport{}
+const ID = "/noise"
+const maxProtoNum = 100
 
-// Transport implements the interface sec.SecureTransport
-// https://godoc.org/github.com/libp2p/go-libp2p/core/sec#SecureConn
 type Transport struct {
+	protocolID protocol.ID
 	localID    peer.ID
 	privateKey crypto.PrivKey
 	muxers     []string
 }
 
+var _ sec.SecureTransport = &Transport{}
+
 // New creates a new Noise transport using the given private key as its
 // libp2p identity key.
-func New(privkey crypto.PrivKey, muxers []protocol.ID) (*Transport, error) {
+func New(id protocol.ID, privkey crypto.PrivKey, muxers []tptu.StreamMuxer) (*Transport, error) {
 	localID, err := peer.IDFromPrivateKey(privkey)
 	if err != nil {
 		return nil, err
 	}
 
 	smuxers := make([]string, 0, len(muxers))
-	for _, muxer := range muxers {
-		smuxers = append(smuxers, string(muxer))
+	for _, m := range muxers {
+		smuxers = append(smuxers, string(m.ID))
 	}
 
 	return &Transport{
+		protocolID: id,
 		localID:    localID,
 		privateKey: privkey,
 		muxers:     smuxers,
@@ -54,7 +53,7 @@ func New(privkey crypto.PrivKey, muxers []protocol.ID) (*Transport, error) {
 // If p is empty, connections from any peer are accepted.
 func (t *Transport) SecureInbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
 	responderEDH := newTransportEDH(t)
-	c, err := newSecureSession(t, ctx, insecure, p, nil, nil, responderEDH, false)
+	c, err := newSecureSession(t, ctx, insecure, p, nil, nil, responderEDH, false, p != "")
 	if err != nil {
 		addr, maErr := manet.FromNetAddr(insecure.RemoteAddr())
 		if maErr == nil {
@@ -67,15 +66,15 @@ func (t *Transport) SecureInbound(ctx context.Context, insecure net.Conn, p peer
 // SecureOutbound runs the Noise handshake as the initiator.
 func (t *Transport) SecureOutbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
 	initiatorEDH := newTransportEDH(t)
-	c, err := newSecureSession(t, ctx, insecure, p, nil, initiatorEDH, nil, true)
+	c, err := newSecureSession(t, ctx, insecure, p, nil, initiatorEDH, nil, true, true)
 	if err != nil {
 		return c, err
 	}
 	return SessionWithConnState(c, initiatorEDH.MatchMuxers(true)), err
 }
 
-func (t *Transport) WithSessionOptions(opts ...SessionOption) (sec.SecureTransport, error) {
-	st := &SessionTransport{t: t}
+func (t *Transport) WithSessionOptions(opts ...SessionOption) (*SessionTransport, error) {
+	st := &SessionTransport{t: t, protocolID: t.protocolID}
 	for _, opt := range opts {
 		if err := opt(st); err != nil {
 			return nil, err
@@ -84,11 +83,15 @@ func (t *Transport) WithSessionOptions(opts ...SessionOption) (sec.SecureTranspo
 	return st, nil
 }
 
+func (t *Transport) ID() protocol.ID {
+	return t.protocolID
+}
+
 func matchMuxers(initiatorMuxers, responderMuxers []string) string {
-	for _, muxer := range responderMuxers {
-		for _, initMuxer := range initiatorMuxers {
-			if initMuxer == muxer {
-				return muxer
+	for _, initMuxer := range initiatorMuxers {
+		for _, respMuxer := range responderMuxers {
+			if initMuxer == respMuxer {
+				return initMuxer
 			}
 		}
 	}
diff --git a/p2p/security/tls/cmd/tlsdiag/client.go b/p2p/security/tls/cmd/tlsdiag/client.go
index 2292bfe0..3868afeb 100644
--- a/p2p/security/tls/cmd/tlsdiag/client.go
+++ b/p2p/security/tls/cmd/tlsdiag/client.go
@@ -34,7 +34,7 @@ func StartClient() error {
 		return err
 	}
 	fmt.Printf(" Peer ID: %s\n", id.Pretty())
-	tp, err := libp2ptls.New(priv, nil)
+	tp, err := libp2ptls.New(libp2ptls.ID, priv, nil)
 	if err != nil {
 		return err
 	}
diff --git a/p2p/security/tls/cmd/tlsdiag/server.go b/p2p/security/tls/cmd/tlsdiag/server.go
index 05e4be3f..76c45a15 100644
--- a/p2p/security/tls/cmd/tlsdiag/server.go
+++ b/p2p/security/tls/cmd/tlsdiag/server.go
@@ -27,7 +27,7 @@ func StartServer() error {
 		return err
 	}
 	fmt.Printf(" Peer ID: %s\n", id.Pretty())
-	tp, err := libp2ptls.New(priv, nil)
+	tp, err := libp2ptls.New(libp2ptls.ID, priv, nil)
 	if err != nil {
 		return err
 	}
diff --git a/p2p/security/tls/transport.go b/p2p/security/tls/transport.go
index 695f6484..a78abec2 100644
--- a/p2p/security/tls/transport.go
+++ b/p2p/security/tls/transport.go
@@ -15,6 +15,7 @@ import (
 	"github.com/libp2p/go-libp2p/core/peer"
 	"github.com/libp2p/go-libp2p/core/protocol"
 	"github.com/libp2p/go-libp2p/core/sec"
+	tptu "github.com/libp2p/go-libp2p/p2p/net/upgrader"
 
 	manet "github.com/multiformats/go-multiaddr/net"
 )
@@ -26,21 +27,29 @@ const ID = "/tls/1.0.0"
 type Transport struct {
 	identity *Identity
 
-	localPeer peer.ID
-	privKey   ci.PrivKey
-	muxers    []protocol.ID
+	localPeer  peer.ID
+	privKey    ci.PrivKey
+	muxers     []protocol.ID
+	protocolID protocol.ID
 }
 
+var _ sec.SecureTransport = &Transport{}
+
 // New creates a TLS encrypted transport
-func New(key ci.PrivKey, muxers []protocol.ID) (*Transport, error) {
-	id, err := peer.IDFromPrivateKey(key)
+func New(id protocol.ID, key ci.PrivKey, muxers []tptu.StreamMuxer) (*Transport, error) {
+	localPeer, err := peer.IDFromPrivateKey(key)
 	if err != nil {
 		return nil, err
 	}
+	muxerIDs := make([]protocol.ID, 0, len(muxers))
+	for _, m := range muxers {
+		muxerIDs = append(muxerIDs, m.ID)
+	}
 	t := &Transport{
-		localPeer: id,
-		privKey:   key,
-		muxers:    muxers,
+		protocolID: id,
+		localPeer:  localPeer,
+		privKey:    key,
+		muxers:     muxerIDs,
 	}
 
 	identity, err := NewIdentity(key)
@@ -51,8 +60,6 @@ func New(key ci.PrivKey, muxers []protocol.ID) (*Transport, error) {
 	return t, nil
 }
 
-var _ sec.SecureTransport = &Transport{}
-
 // SecureInbound runs the TLS handshake as a server.
 // If p is empty, connections from any peer are accepted.
 func (t *Transport) SecureInbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
@@ -61,7 +68,26 @@ func (t *Transport) SecureInbound(ctx context.Context, insecure net.Conn, p peer
 	for _, muxer := range t.muxers {
 		muxers = append(muxers, string(muxer))
 	}
-	// Prepend the prefered muxers list to TLS config.
+	// TLS' ALPN selection lets the server select the protocol, preferring the server's preferences.
+	// We want to prefer the client's preference though.
+	getConfigForClient := config.GetConfigForClient
+	config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
+	alpnLoop:
+		for _, proto := range info.SupportedProtos {
+			for _, m := range muxers {
+				if m == proto {
+					// Match found. Select this muxer, as it's the client's preference.
+					// There's no need to add the "libp2p" entry here.
+					config.NextProtos = []string{proto}
+					break alpnLoop
+				}
+			}
+		}
+		if getConfigForClient != nil {
+			return getConfigForClient(info)
+		}
+		return config, nil
+	}
 	config.NextProtos = append(muxers, config.NextProtos...)
 	cs, err := t.handshake(ctx, tls.Server(insecure, config), keyCh)
 	if err != nil {
@@ -148,3 +174,7 @@ func (t *Transport) setupConn(tlsConn *tls.Conn, remotePubKey ci.PubKey) (sec.Se
 		connectionState: network.ConnectionState{NextProto: nextProto},
 	}, nil
 }
+
+func (t *Transport) ID() protocol.ID {
+	return t.protocolID
+}
diff --git a/p2p/transport/quic/transport.go b/p2p/transport/quic/transport.go
index 0a0af9fc..29a22c20 100644
--- a/p2p/transport/quic/transport.go
+++ b/p2p/transport/quic/transport.go
@@ -197,10 +197,11 @@ func NewTransport(key ic.PrivKey, psk pnet.PSK, gater connmgr.ConnectionGater, r
 		return nil, err
 	}
 	keyReader := hkdf.New(sha256.New, keyBytes, nil, []byte(statelessResetKeyInfo))
-	qconfig.StatelessResetKey = make([]byte, 32)
-	if _, err := io.ReadFull(keyReader, qconfig.StatelessResetKey); err != nil {
+	var statelessResetKey quic.StatelessResetKey
+	if _, err := io.ReadFull(keyReader, statelessResetKey[:]); err != nil {
 		return nil, err
 	}
+	qconfig.StatelessResetKey = &statelessResetKey
 	var tracers []quiclogging.Tracer
 	if qlogTracer := quicutils.QLOGTracer; qlogTracer != nil {
 		tracers = append(tracers, qlogTracer)
diff --git a/p2p/transport/webtransport/cert_manager.go b/p2p/transport/webtransport/cert_manager.go
index da2c69f4..9d9da188 100644
--- a/p2p/transport/webtransport/cert_manager.go
+++ b/p2p/transport/webtransport/cert_manager.go
@@ -4,11 +4,13 @@ import (
 	"context"
 	"crypto/sha256"
 	"crypto/tls"
+	"encoding/binary"
 	"fmt"
 	"sync"
 	"time"
 
 	"github.com/benbjohnson/clock"
+	ic "github.com/libp2p/go-libp2p/core/crypto"
 	ma "github.com/multiformats/go-multiaddr"
 	"github.com/multiformats/go-multihash"
 )
@@ -17,6 +19,7 @@ import (
 // When we generate a certificate, the NotBefore time is set to clockSkewAllowance before the current time.
 // Similarly, we stop using a certificate one clockSkewAllowance before its expiry time.
 const clockSkewAllowance = time.Hour
+const validityMinusTwoSkew = certValidity - (2 * clockSkewAllowance)
 
 type certConfig struct {
 	tlsConf *tls.Config
@@ -26,8 +29,8 @@ type certConfig struct {
 func (c *certConfig) Start() time.Time { return c.tlsConf.Certificates[0].Leaf.NotBefore }
 func (c *certConfig) End() time.Time   { return c.tlsConf.Certificates[0].Leaf.NotAfter }
 
-func newCertConfig(start, end time.Time) (*certConfig, error) {
-	conf, err := getTLSConf(start, end)
+func newCertConfig(key ic.PrivKey, start, end time.Time) (*certConfig, error) {
+	conf, err := getTLSConf(key, start, end)
 	if err != nil {
 		return nil, err
 	}
@@ -57,32 +60,58 @@ type certManager struct {
 	serializedCertHashes [][]byte
 }
 
-func newCertManager(clock clock.Clock) (*certManager, error) {
+func newCertManager(hostKey ic.PrivKey, clock clock.Clock) (*certManager, error) {
 	m := &certManager{clock: clock}
 	m.ctx, m.ctxCancel = context.WithCancel(context.Background())
-	if err := m.init(); err != nil {
+	if err := m.init(hostKey); err != nil {
 		return nil, err
 	}
 
-	m.background()
+	m.background(hostKey)
 	return m, nil
 }
 
-func (m *certManager) init() error {
-	start := m.clock.Now().Add(-clockSkewAllowance)
-	var err error
-	m.nextConfig, err = newCertConfig(start, start.Add(certValidity))
+// getCurrentTimeBucket returns the canonical start time of the given time as
+// bucketed by ranges of certValidity since unix epoch (plus an offset). This
+// lets you get the same time ranges across reboots without having to persist
+// state.
+// ```
+// ... v--- epoch + offset
+// ... |--------|    |--------|        ...
+// ...        |--------|    |--------| ...
+// ```
+func getCurrentBucketStartTime(now time.Time, offset time.Duration) time.Time {
+	currentBucket := (now.UnixMilli() - offset.Milliseconds()) / validityMinusTwoSkew.Milliseconds()
+	return time.UnixMilli(offset.Milliseconds() + currentBucket*validityMinusTwoSkew.Milliseconds())
+}
+
+func (m *certManager) init(hostKey ic.PrivKey) error {
+	start := m.clock.Now()
+	pubkeyBytes, err := hostKey.GetPublic().Raw()
+	if err != nil {
+		return err
+	}
+
+	// We want to add a random offset to each start time so that not all certs
+	// rotate at the same time across the network. The offset represents moving
+	// the bucket start time some `offset` earlier.
+	offset := (time.Duration(binary.LittleEndian.Uint16(pubkeyBytes)) * time.Minute) % certValidity
+
+	// We want the certificate have been valid for at least one clockSkewAllowance
+	start = start.Add(-clockSkewAllowance)
+	startTime := getCurrentBucketStartTime(start, offset)
+	m.nextConfig, err = newCertConfig(hostKey, startTime, startTime.Add(certValidity))
 	if err != nil {
 		return err
 	}
-	return m.rollConfig()
+	return m.rollConfig(hostKey)
 }
 
-func (m *certManager) rollConfig() error {
+func (m *certManager) rollConfig(hostKey ic.PrivKey) error {
 	// We stop using the current certificate clockSkewAllowance before its expiry time.
 	// At this point, the next certificate needs to be valid for one clockSkewAllowance.
 	nextStart := m.nextConfig.End().Add(-2 * clockSkewAllowance)
-	c, err := newCertConfig(nextStart, nextStart.Add(certValidity))
+	c, err := newCertConfig(hostKey, nextStart, nextStart.Add(certValidity))
 	if err != nil {
 		return err
 	}
@@ -95,7 +124,7 @@ func (m *certManager) rollConfig() error {
 	return m.cacheAddrComponent()
 }
 
-func (m *certManager) background() {
+func (m *certManager) background(hostKey ic.PrivKey) {
 	d := m.currentConfig.End().Add(-clockSkewAllowance).Sub(m.clock.Now())
 	log.Debugw("setting timer", "duration", d.String())
 	t := m.clock.Timer(d)
@@ -111,7 +140,7 @@ func (m *certManager) background() {
 				return
 			case now := <-t.C:
 				m.mx.Lock()
-				if err := m.rollConfig(); err != nil {
+				if err := m.rollConfig(hostKey); err != nil {
 					log.Errorw("rolling config failed", "error", err)
 				}
 				d := m.currentConfig.End().Add(-clockSkewAllowance).Sub(now)
diff --git a/p2p/transport/webtransport/crypto.go b/p2p/transport/webtransport/crypto.go
index 0ea71323..dc2c1f03 100644
--- a/p2p/transport/webtransport/crypto.go
+++ b/p2p/transport/webtransport/crypto.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"crypto/ecdsa"
 	"crypto/elliptic"
-	"crypto/rand"
 	"crypto/sha256"
 	"crypto/tls"
 	"crypto/x509"
@@ -12,14 +11,20 @@ import (
 	"encoding/binary"
 	"errors"
 	"fmt"
+	"io"
 	"math/big"
 	"time"
 
+	ic "github.com/libp2p/go-libp2p/core/crypto"
+
 	"github.com/multiformats/go-multihash"
+	"golang.org/x/crypto/hkdf"
 )
 
-func getTLSConf(start, end time.Time) (*tls.Config, error) {
-	cert, priv, err := generateCert(start, end)
+const deterministicCertInfo = "determinisitic cert"
+
+func getTLSConf(key ic.PrivKey, start, end time.Time) (*tls.Config, error) {
+	cert, priv, err := generateCert(key, start, end)
 	if err != nil {
 		return nil, err
 	}
@@ -32,9 +37,20 @@ func getTLSConf(start, end time.Time) (*tls.Config, error) {
 	}, nil
 }
 
-func generateCert(start, end time.Time) (*x509.Certificate, *ecdsa.PrivateKey, error) {
+// generateCert generates certs deterministically based on the `key` and start
+// time passed in. Uses `golang.org/x/crypto/hkdf`.
+func generateCert(key ic.PrivKey, start, end time.Time) (*x509.Certificate, *ecdsa.PrivateKey, error) {
+	keyBytes, err := key.Raw()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	startTimeSalt := make([]byte, 8)
+	binary.LittleEndian.PutUint64(startTimeSalt, uint64(start.UnixNano()))
+	deterministicHKDFReader := newDeterministicReader(keyBytes, startTimeSalt, deterministicCertInfo)
+
 	b := make([]byte, 8)
-	if _, err := rand.Read(b); err != nil {
+	if _, err := deterministicHKDFReader.Read(b); err != nil {
 		return nil, nil, err
 	}
 	serial := int64(binary.BigEndian.Uint64(b))
@@ -51,11 +67,12 @@ func generateCert(start, end time.Time) (*x509.Certificate, *ecdsa.PrivateKey, e
 		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
 		BasicConstraintsValid: true,
 	}
-	caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+
+	caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), deterministicHKDFReader)
 	if err != nil {
 		return nil, nil, err
 	}
-	caBytes, err := x509.CreateCertificate(rand.Reader, certTempl, certTempl, &caPrivateKey.PublicKey, caPrivateKey)
+	caBytes, err := x509.CreateCertificate(deterministicHKDFReader, certTempl, certTempl, caPrivateKey.Public(), caPrivateKey)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -106,3 +123,30 @@ func verifyRawCerts(rawCerts [][]byte, certHashes []multihash.DecodedMultihash)
 	}
 	return nil
 }
+
+// deterministicReader is a hack. It counter-acts the Go library's attempt at
+// making ECDSA signatures non-deterministic. Go adds non-determinism by
+// randomly dropping a singly byte from the reader stream. This counteracts this
+// by detecting when a read is a single byte and using a different reader
+// instead.
+type deterministicReader struct {
+	reader           io.Reader
+	singleByteReader io.Reader
+}
+
+func newDeterministicReader(seed []byte, salt []byte, info string) io.Reader {
+	reader := hkdf.New(sha256.New, seed, salt, []byte(info))
+	singleByteReader := hkdf.New(sha256.New, seed, salt, []byte(info+" single byte"))
+
+	return &deterministicReader{
+		reader:           reader,
+		singleByteReader: singleByteReader,
+	}
+}
+
+func (r *deterministicReader) Read(p []byte) (n int, err error) {
+	if len(p) == 1 {
+		return r.singleByteReader.Read(p)
+	}
+	return r.reader.Read(p)
+}
diff --git a/p2p/transport/webtransport/transport.go b/p2p/transport/webtransport/transport.go
index 3d902b4e..8e6ac77d 100644
--- a/p2p/transport/webtransport/transport.go
+++ b/p2p/transport/webtransport/transport.go
@@ -110,7 +110,7 @@ func New(key ic.PrivKey, gater connmgr.ConnectionGater, rcmgr network.ResourceMa
 			return nil, err
 		}
 	}
-	n, err := noise.New(key, nil)
+	n, err := noise.New(noise.ID, key, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -293,7 +293,7 @@ func (t *transport) Listen(laddr ma.Multiaddr) (tpt.Listener, error) {
 	}
 	if t.staticTLSConf == nil {
 		t.listenOnce.Do(func() {
-			t.certManager, t.listenOnceErr = newCertManager(t.clock)
+			t.certManager, t.listenOnceErr = newCertManager(t.privKey, t.clock)
 		})
 		if t.listenOnceErr != nil {
 			return nil, t.listenOnceErr


Test patch:
diff --git a/config/muxer_test.go b/config/muxer_test.go
deleted file mode 100644
index 772b4329..00000000
--- a/config/muxer_test.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package config
-
-import (
-	"testing"
-
-	"github.com/libp2p/go-libp2p/core/host"
-	"github.com/libp2p/go-libp2p/core/network"
-	"github.com/libp2p/go-libp2p/core/peer"
-	bhost "github.com/libp2p/go-libp2p/p2p/host/basic"
-	"github.com/libp2p/go-libp2p/p2p/muxer/yamux"
-	swarmt "github.com/libp2p/go-libp2p/p2p/net/swarm/testing"
-)
-
-func TestMuxerSimple(t *testing.T) {
-	// single
-	_, err := MuxerConstructor(func(_ peer.ID) network.Multiplexer { return nil })
-	if err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestMuxerByValue(t *testing.T) {
-	_, err := MuxerConstructor(yamux.DefaultTransport)
-	if err != nil {
-		t.Fatal(err)
-	}
-}
-func TestMuxerDuplicate(t *testing.T) {
-	_, err := MuxerConstructor(func(_ peer.ID, _ peer.ID) network.Multiplexer { return nil })
-	if err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestMuxerError(t *testing.T) {
-	_, err := MuxerConstructor(func() (network.Multiplexer, error) { return nil, nil })
-	if err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestMuxerBadTypes(t *testing.T) {
-	for i, f := range []interface{}{
-		func() error { return nil },
-		func() string { return "" },
-		func() {},
-		func(string) network.Multiplexer { return nil },
-		func(string) (network.Multiplexer, error) { return nil, nil },
-		nil,
-		"testing",
-	} {
-
-		if _, err := MuxerConstructor(f); err == nil {
-			t.Fatalf("constructor %d with type %T should have failed", i, f)
-		}
-	}
-}
-
-func TestCatchDuplicateTransportsMuxer(t *testing.T) {
-	h, err := bhost.NewHost(swarmt.GenSwarm(t), nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	yamuxMuxer, err := MuxerConstructor(yamux.DefaultTransport)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	var tests = map[string]struct {
-		h             host.Host
-		transports    []MsMuxC
-		expectedError string
-	}{
-		"no duplicate transports": {
-			h:             h,
-			transports:    []MsMuxC{{yamuxMuxer, "yamux"}},
-			expectedError: "",
-		},
-		"duplicate transports": {
-			h: h,
-			transports: []MsMuxC{
-				{yamuxMuxer, "yamux"},
-				{yamuxMuxer, "yamux"},
-			},
-			expectedError: "duplicate muxer transport: yamux",
-		},
-	}
-	for testName, test := range tests {
-		t.Run(testName, func(t *testing.T) {
-			_, err = makeMuxer(test.h, test.transports)
-			if err != nil {
-				if err.Error() != test.expectedError {
-					t.Errorf(
-						"\nexpected: [%v]\nactual:   [%v]\n",
-						test.expectedError,
-						err,
-					)
-				}
-			}
-		})
-	}
-}
diff --git a/config/reflection_magic_test.go b/config/reflection_magic_test.go
deleted file mode 100644
index ce9f3986..00000000
--- a/config/reflection_magic_test.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package config
-
-import (
-	"errors"
-	"reflect"
-	"strings"
-	"testing"
-)
-
-func TestHandleReturnValue(t *testing.T) {
-	// one value
-	v, err := handleReturnValue([]reflect.Value{reflect.ValueOf(1)})
-	if v.(int) != 1 {
-		t.Fatal("expected value")
-	}
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Nil value
-	v, err = handleReturnValue([]reflect.Value{reflect.ValueOf(nil)})
-	if v != nil {
-		t.Fatal("expected no value")
-	}
-	if err == nil {
-		t.Fatal("expected an error")
-	}
-
-	// Nil value, nil err
-	v, err = handleReturnValue([]reflect.Value{reflect.ValueOf(nil), reflect.ValueOf(nil)})
-	if v != nil {
-		t.Fatal("expected no value")
-	}
-	if err == nil {
-		t.Fatal("expected an error")
-	}
-
-	// two values
-	v, err = handleReturnValue([]reflect.Value{reflect.ValueOf(1), reflect.ValueOf(nil)})
-	if v, ok := v.(int); !ok || v != 1 {
-		t.Fatalf("expected value of 1, got %v", v)
-	}
-	if err != nil {
-		t.Fatal("expected no error")
-	}
-
-	// an error
-	myError := errors.New("my error")
-	_, err = handleReturnValue([]reflect.Value{reflect.ValueOf(1), reflect.ValueOf(myError)})
-	if err != myError {
-		t.Fatal(err)
-	}
-
-	for _, vals := range [][]reflect.Value{
-		{reflect.ValueOf(1), reflect.ValueOf("not an error")},
-		{},
-		{reflect.ValueOf(1), reflect.ValueOf(myError), reflect.ValueOf(myError)},
-	} {
-		func() {
-			defer func() { recover() }()
-			handleReturnValue(vals)
-			t.Fatal("expected a panic")
-		}()
-	}
-}
-
-type foo interface {
-	foo() foo
-}
-
-var fooType = reflect.TypeOf((*foo)(nil)).Elem()
-
-func TestCheckReturnType(t *testing.T) {
-	for i, fn := range []interface{}{
-		func() { panic("") },
-		func() error { panic("") },
-		func() (error, error) { panic("") },
-		func() (foo, error, error) { panic("") },
-		func() (foo, foo) { panic("") },
-	} {
-		if checkReturnType(reflect.TypeOf(fn), fooType) == nil {
-			t.Errorf("expected falure for case %d (type %T)", i, fn)
-		}
-	}
-
-	for i, fn := range []interface{}{
-		func() foo { panic("") },
-		func() (foo, error) { panic("") },
-	} {
-		if err := checkReturnType(reflect.TypeOf(fn), fooType); err != nil {
-			t.Errorf("expected success for case %d (type %T), got: %s", i, fn, err)
-		}
-	}
-}
-
-func constructFoo() foo {
-	return nil
-}
-
-type fooImpl struct{}
-
-func (f *fooImpl) foo() foo { return nil }
-
-func TestCallConstructor(t *testing.T) {
-	_, err := callConstructor(reflect.ValueOf(constructFoo), nil)
-	if err == nil {
-		t.Fatal("expected constructor to fail")
-	}
-
-	if !strings.Contains(err.Error(), "constructFoo") {
-		t.Errorf("expected error to contain the constructor name: %s", err)
-	}
-
-	v, err := callConstructor(reflect.ValueOf(func() foo { return &fooImpl{} }), nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if _, ok := v.(*fooImpl); !ok {
-		t.Fatal("expected a fooImpl")
-	}
-
-	v, err = callConstructor(reflect.ValueOf(func() *fooImpl { return new(fooImpl) }), nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if _, ok := v.(*fooImpl); !ok {
-		t.Fatal("expected a fooImpl")
-	}
-
-	_, err = callConstructor(reflect.ValueOf(func() (*fooImpl, error) { return nil, nil }), nil)
-	if err == nil {
-		t.Fatal("expected error")
-	}
-
-	v, err = callConstructor(reflect.ValueOf(func() (*fooImpl, error) { return new(fooImpl), nil }), nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if _, ok := v.(*fooImpl); !ok {
-		t.Fatal("expected a fooImpl")
-	}
-}
diff --git a/config/transport_test.go b/config/transport_test.go
deleted file mode 100644
index 5684521e..00000000
--- a/config/transport_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package config
-
-import (
-	"testing"
-
-	"github.com/libp2p/go-libp2p/core/peer"
-	"github.com/libp2p/go-libp2p/core/transport"
-	"github.com/libp2p/go-libp2p/p2p/transport/tcp"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestTransportVariadicOptions(t *testing.T) {
-	_, err := TransportConstructor(func(_ peer.ID, _ ...int) transport.Transport { return nil })
-	require.NoError(t, err)
-}
-
-func TestConstructorWithoutOptsCalledWithOpts(t *testing.T) {
-	_, err := TransportConstructor(func(_ transport.Upgrader) transport.Transport {
-		return nil
-	}, 42)
-	require.EqualError(t, err, "constructor doesn't accept any options")
-}
-
-func TestConstructorWithOptsTypeMismatch(t *testing.T) {
-	_, err := TransportConstructor(func(_ transport.Upgrader, opts ...int) transport.Transport {
-		return nil
-	}, 42, "foo")
-	require.EqualError(t, err, "expected option of type int, got string")
-}
-
-func TestConstructorWithOpts(t *testing.T) {
-	var options []int
-	c, err := TransportConstructor(func(_ transport.Upgrader, opts ...int) (transport.Transport, error) {
-		options = opts
-		return tcp.NewTCPTransport(nil, nil)
-	}, 42, 1337)
-	require.NoError(t, err)
-	_, err = c(nil, nil, nil, nil, nil, nil)
-	require.NoError(t, err)
-	require.Equal(t, []int{42, 1337}, options)
-}
diff --git a/core/sec/insecure/insecure_test.go b/core/sec/insecure/insecure_test.go
index a3ce8314..8663f1d9 100644
--- a/core/sec/insecure/insecure_test.go
+++ b/core/sec/insecure/insecure_test.go
@@ -61,7 +61,7 @@ func newTestTransport(t *testing.T, typ, bits int) *Transport {
 	require.NoError(t, err)
 	id, err := peer.IDFromPublicKey(pub)
 	require.NoError(t, err)
-	return NewWithIdentity(id, priv)
+	return NewWithIdentity("/test/1.0.0", id, priv)
 }
 
 // Create a new pair of connected TCP sockets.
diff --git a/libp2p_test.go b/libp2p_test.go
index 094fdefb..9a469bd1 100644
--- a/libp2p_test.go
+++ b/libp2p_test.go
@@ -12,8 +12,13 @@ import (
 	"github.com/libp2p/go-libp2p/core/host"
 	"github.com/libp2p/go-libp2p/core/peer"
 	"github.com/libp2p/go-libp2p/core/transport"
+	"github.com/libp2p/go-libp2p/p2p/net/swarm"
+	"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/tcp"
 
+	ma "github.com/multiformats/go-multiaddr"
 	"github.com/stretchr/testify/require"
 )
 
@@ -165,3 +170,122 @@ func TestChainOptions(t *testing.T) {
 		}
 	}
 }
+
+func TestTransportConstructorTCP(t *testing.T) {
+	h, err := New(
+		Transport(tcp.NewTCPTransport),
+		DisableRelay(),
+	)
+	require.NoError(t, err)
+	defer h.Close()
+	require.NoError(t, h.Network().Listen(ma.StringCast("/ip4/127.0.0.1/tcp/0")))
+	err = h.Network().Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic"))
+	require.Error(t, err)
+	require.Contains(t, err.Error(), swarm.ErrNoTransport.Error())
+}
+
+func TestTransportConstructorQUIC(t *testing.T) {
+	h, err := New(
+		Transport(quic.NewTransport, quic.DisableReuseport()),
+		DisableRelay(),
+	)
+	require.NoError(t, err)
+	defer h.Close()
+	require.NoError(t, h.Network().Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic")))
+	err = h.Network().Listen(ma.StringCast("/ip4/127.0.0.1/tcp/0"))
+	require.Error(t, err)
+	require.Contains(t, err.Error(), swarm.ErrNoTransport.Error())
+}
+
+type mockTransport struct{}
+
+func (m mockTransport) Dial(context.Context, ma.Multiaddr, peer.ID) (transport.CapableConn, error) {
+	panic("implement me")
+}
+
+func (m mockTransport) CanDial(ma.Multiaddr) bool                       { panic("implement me") }
+func (m mockTransport) Listen(ma.Multiaddr) (transport.Listener, error) { panic("implement me") }
+func (m mockTransport) Protocols() []int                                { return []int{1337} }
+func (m mockTransport) Proxy() bool                                     { panic("implement me") }
+
+var _ transport.Transport = &mockTransport{}
+
+func TestTransportConstructorWithoutOpts(t *testing.T) {
+	t.Run("successful", func(t *testing.T) {
+		var called bool
+		constructor := func() transport.Transport {
+			called = true
+			return &mockTransport{}
+		}
+
+		h, err := New(
+			Transport(constructor),
+			DisableRelay(),
+		)
+		require.NoError(t, err)
+		require.True(t, called, "expected constructor to be called")
+		defer h.Close()
+	})
+
+	t.Run("with options", func(t *testing.T) {
+		var called bool
+		constructor := func() transport.Transport {
+			called = true
+			return &mockTransport{}
+		}
+
+		_, err := New(
+			Transport(constructor, tcp.DisableReuseport()),
+			DisableRelay(),
+		)
+		require.EqualError(t, err, "transport constructor doesn't take any options")
+		require.False(t, called, "didn't expected constructor to be called")
+	})
+}
+
+func TestTransportConstructorWithWrongOpts(t *testing.T) {
+	_, err := New(
+		Transport(quic.NewTransport, tcp.DisableReuseport()),
+		DisableRelay(),
+	)
+	require.EqualError(t, err, "transport option of type tcp.Option not assignable to libp2pquic.Option")
+}
+
+func TestSecurityConstructor(t *testing.T) {
+	h, err := New(
+		Transport(tcp.NewTCPTransport),
+		Security("/noisy", noise.New),
+		Security("/tls", tls.New),
+		DefaultListenAddrs,
+		DisableRelay(),
+	)
+	require.NoError(t, err)
+	defer h.Close()
+
+	h1, err := New(
+		NoListenAddrs,
+		Transport(tcp.NewTCPTransport),
+		Security("/noise", noise.New), // different name
+		DisableRelay(),
+	)
+	require.NoError(t, err)
+	defer h1.Close()
+
+	h2, err := New(
+		NoListenAddrs,
+		Transport(tcp.NewTCPTransport),
+		Security("/noisy", noise.New),
+		DisableRelay(),
+	)
+	require.NoError(t, err)
+	defer h2.Close()
+
+	ai := peer.AddrInfo{
+		ID:    h.ID(),
+		Addrs: h.Addrs(),
+	}
+	err = h1.Connect(context.Background(), ai)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "failed to negotiate security protocol")
+	require.NoError(t, h2.Connect(context.Background(), ai))
+}
diff --git a/p2p/discovery/routing/routing_test.go b/p2p/discovery/routing/routing_test.go
index ed8ab25d..6e28c06f 100644
--- a/p2p/discovery/routing/routing_test.go
+++ b/p2p/discovery/routing/routing_test.go
@@ -135,7 +135,7 @@ func TestDiscoveryRouting(t *testing.T) {
 
 	pch := r2.FindProvidersAsync(ctx, c, 20)
 
-	var allAIs []peer.AddrInfo
+	allAIs := make([]peer.AddrInfo, 0, len(pch))
 	for ai := range pch {
 		allAIs = append(allAIs, ai)
 	}
diff --git a/p2p/host/autorelay/autorelay_test.go b/p2p/host/autorelay/autorelay_test.go
index e7ac6d99..bbdec41d 100644
--- a/p2p/host/autorelay/autorelay_test.go
+++ b/p2p/host/autorelay/autorelay_test.go
@@ -444,3 +444,15 @@ func TestMaxAge(t *testing.T) {
 	}
 	require.Contains(t, ids, relays[0])
 }
+
+func TestIncorrectInit(t *testing.T) {
+	// Check if we panic if we do not correctly initialize the autorelay system.
+	// Common since it's easy to initialize without passing in the correct options: https://github.com/libp2p/go-libp2p/issues/1852
+
+	defer func() {
+		if r := recover(); r == nil {
+			t.Errorf("Expected to panic")
+		}
+	}()
+	_ = newPrivateNode(t)
+}
diff --git a/p2p/net/conn-security-multistream/ssms_test.go b/p2p/net/conn-security-multistream/ssms_test.go
index 5aa5db35..3ccf4a7f 100644
--- a/p2p/net/conn-security-multistream/ssms_test.go
+++ b/p2p/net/conn-security-multistream/ssms_test.go
@@ -44,8 +44,8 @@ func TestCommonProto(t *testing.T) {
 
 	var at, bt SSMuxer
 
-	atInsecure := insecure.NewWithIdentity(idA, privA)
-	btInsecure := insecure.NewWithIdentity(idB, privB)
+	atInsecure := insecure.NewWithIdentity(insecure.ID, idA, privA)
+	btInsecure := insecure.NewWithIdentity(insecure.ID, idB, privB)
 	at.AddTransport("/plaintext/1.0.0", atInsecure)
 	bt.AddTransport("/plaintext/1.1.0", btInsecure)
 	bt.AddTransport("/plaintext/1.0.0", btInsecure)
@@ -88,8 +88,8 @@ func TestNoCommonProto(t *testing.T) {
 	privB, idB := newPeer(t)
 
 	var at, bt SSMuxer
-	atInsecure := insecure.NewWithIdentity(idA, privA)
-	btInsecure := insecure.NewWithIdentity(idB, privB)
+	atInsecure := insecure.NewWithIdentity(insecure.ID, idA, privA)
+	btInsecure := insecure.NewWithIdentity(insecure.ID, idB, privB)
 
 	at.AddTransport("/plaintext/1.0.0", atInsecure)
 	bt.AddTransport("/plaintext/1.1.0", btInsecure)
diff --git a/p2p/net/connmgr/connmgr_test.go b/p2p/net/connmgr/connmgr_test.go
index 2053e3e6..39db7b54 100644
--- a/p2p/net/connmgr/connmgr_test.go
+++ b/p2p/net/connmgr/connmgr_test.go
@@ -808,36 +808,57 @@ func (m mockConn) GetStreams() []network.Stream                          { panic
 func (m mockConn) Scope() network.ConnScope                              { panic("implement me") }
 func (m mockConn) ConnState() network.ConnectionState                    { return network.ConnectionState{} }
 
+func makeSegmentsWithPeerInfos(peerInfos peerInfos) *segments {
+	var s = func() *segments {
+		ret := segments{}
+		for i := range ret.buckets {
+			ret.buckets[i] = &segment{
+				peers: make(map[peer.ID]*peerInfo),
+			}
+		}
+		return &ret
+	}()
+
+	for _, pi := range peerInfos {
+		segment := s.get(pi.id)
+		segment.Lock()
+		segment.peers[pi.id] = pi
+		segment.Unlock()
+	}
+
+	return s
+}
+
 func TestPeerInfoSorting(t *testing.T) {
 	t.Run("starts with temporary connections", func(t *testing.T) {
-		p1 := peerInfo{id: peer.ID("peer1")}
-		p2 := peerInfo{id: peer.ID("peer2"), temp: true}
+		p1 := &peerInfo{id: peer.ID("peer1")}
+		p2 := &peerInfo{id: peer.ID("peer2"), temp: true}
 		pis := peerInfos{p1, p2}
-		pis.SortByValueAndStreams(false)
+		pis.SortByValueAndStreams(makeSegmentsWithPeerInfos(pis), false)
 		require.Equal(t, pis, peerInfos{p2, p1})
 	})
 
 	t.Run("starts with low-value connections", func(t *testing.T) {
-		p1 := peerInfo{id: peer.ID("peer1"), value: 40}
-		p2 := peerInfo{id: peer.ID("peer2"), value: 20}
+		p1 := &peerInfo{id: peer.ID("peer1"), value: 40}
+		p2 := &peerInfo{id: peer.ID("peer2"), value: 20}
 		pis := peerInfos{p1, p2}
-		pis.SortByValueAndStreams(false)
+		pis.SortByValueAndStreams(makeSegmentsWithPeerInfos(pis), false)
 		require.Equal(t, pis, peerInfos{p2, p1})
 	})
 
 	t.Run("prefer peers with no streams", func(t *testing.T) {
-		p1 := peerInfo{id: peer.ID("peer1"),
+		p1 := &peerInfo{id: peer.ID("peer1"),
 			conns: map[network.Conn]time.Time{
 				&mockConn{stats: network.ConnStats{NumStreams: 0}}: time.Now(),
 			},
 		}
-		p2 := peerInfo{id: peer.ID("peer2"),
+		p2 := &peerInfo{id: peer.ID("peer2"),
 			conns: map[network.Conn]time.Time{
 				&mockConn{stats: network.ConnStats{NumStreams: 1}}: time.Now(),
 			},
 		}
 		pis := peerInfos{p2, p1}
-		pis.SortByValueAndStreams(false)
+		pis.SortByValueAndStreams(makeSegmentsWithPeerInfos(pis), false)
 		require.Equal(t, pis, peerInfos{p1, p2})
 	})
 
@@ -849,27 +870,27 @@ func TestPeerInfoSorting(t *testing.T) {
 
 		outgoingSomeStreams := network.ConnStats{Stats: network.Stats{Direction: network.DirOutbound}, NumStreams: 1}
 		outgoingMoreStreams := network.ConnStats{Stats: network.Stats{Direction: network.DirOutbound}, NumStreams: 2}
-		p1 := peerInfo{
+		p1 := &peerInfo{
 			id: peer.ID("peer1"),
 			conns: map[network.Conn]time.Time{
 				&mockConn{stats: outgoingSomeStreams}: time.Now(),
 			},
 		}
-		p2 := peerInfo{
+		p2 := &peerInfo{
 			id: peer.ID("peer2"),
 			conns: map[network.Conn]time.Time{
 				&mockConn{stats: outgoingSomeStreams}: time.Now(),
 				&mockConn{stats: incoming}:            time.Now(),
 			},
 		}
-		p3 := peerInfo{
+		p3 := &peerInfo{
 			id: peer.ID("peer3"),
 			conns: map[network.Conn]time.Time{
 				&mockConn{stats: outgoing}: time.Now(),
 				&mockConn{stats: incoming}: time.Now(),
 			},
 		}
-		p4 := peerInfo{
+		p4 := &peerInfo{
 			id: peer.ID("peer4"),
 			conns: map[network.Conn]time.Time{
 				&mockConn{stats: outgoingMoreStreams}: time.Now(),
@@ -877,7 +898,7 @@ func TestPeerInfoSorting(t *testing.T) {
 			},
 		}
 		pis := peerInfos{p1, p2, p3, p4}
-		pis.SortByValueAndStreams(true)
+		pis.SortByValueAndStreams(makeSegmentsWithPeerInfos(pis), true)
 		// p3 is first because it is inactive (no streams).
 		// p4 is second because it has the most streams and we priortize killing
 		// connections with the higher number of streams.
@@ -885,13 +906,13 @@ func TestPeerInfoSorting(t *testing.T) {
 	})
 
 	t.Run("in a memory emergency, starts with connections that have many streams", func(t *testing.T) {
-		p1 := peerInfo{
+		p1 := &peerInfo{
 			id: peer.ID("peer1"),
 			conns: map[network.Conn]time.Time{
 				&mockConn{stats: network.ConnStats{NumStreams: 100}}: time.Now(),
 			},
 		}
-		p2 := peerInfo{
+		p2 := &peerInfo{
 			id: peer.ID("peer2"),
 			conns: map[network.Conn]time.Time{
 				&mockConn{stats: network.ConnStats{NumStreams: 80}}: time.Now(),
@@ -899,7 +920,49 @@ func TestPeerInfoSorting(t *testing.T) {
 			},
 		}
 		pis := peerInfos{p1, p2}
-		pis.SortByValueAndStreams(true)
+		pis.SortByValueAndStreams(makeSegmentsWithPeerInfos(pis), true)
 		require.Equal(t, pis, peerInfos{p2, p1})
 	})
 }
+
+func TestSafeConcurrency(t *testing.T) {
+	t.Run("Safe Concurrency", func(t *testing.T) {
+		cl := clock.NewMock()
+
+		p1 := &peerInfo{id: peer.ID("peer1"), conns: map[network.Conn]time.Time{}}
+		p2 := &peerInfo{id: peer.ID("peer2"), conns: map[network.Conn]time.Time{}}
+		pis := peerInfos{p1, p2}
+
+		ss := makeSegmentsWithPeerInfos(pis)
+
+		const runs = 10
+		const concurrency = 10
+		var wg sync.WaitGroup
+		for i := 0; i < concurrency; i++ {
+			wg.Add(1)
+			go func() {
+				// add conns. This mimics new connection events
+				pis := peerInfos{p1, p2}
+				for i := 0; i < runs; i++ {
+					pi := pis[i%len(pis)]
+					s := ss.get(pi.id)
+					s.Lock()
+					s.peers[pi.id].conns[randConn(t, nil)] = cl.Now()
+					s.Unlock()
+				}
+				wg.Done()
+			}()
+
+			wg.Add(1)
+			go func() {
+				pis := peerInfos{p1, p2}
+				for i := 0; i < runs; i++ {
+					pis.SortByValueAndStreams(ss, false)
+				}
+				wg.Done()
+			}()
+		}
+
+		wg.Wait()
+	})
+}
diff --git a/p2p/net/swarm/dial_worker_test.go b/p2p/net/swarm/dial_worker_test.go
index bf7a5ca3..14ff3d64 100644
--- a/p2p/net/swarm/dial_worker_test.go
+++ b/p2p/net/swarm/dial_worker_test.go
@@ -15,7 +15,6 @@ import (
 	"github.com/libp2p/go-libp2p/core/sec/insecure"
 	"github.com/libp2p/go-libp2p/core/transport"
 	"github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem"
-	msmux "github.com/libp2p/go-libp2p/p2p/muxer/muxer-multistream"
 	"github.com/libp2p/go-libp2p/p2p/muxer/yamux"
 	csms "github.com/libp2p/go-libp2p/p2p/net/conn-security-multistream"
 	tptu "github.com/libp2p/go-libp2p/p2p/net/upgrader"
@@ -77,11 +76,9 @@ func makeUpgrader(t *testing.T, n *Swarm) transport.Upgrader {
 	id := n.LocalPeer()
 	pk := n.Peerstore().PrivKey(id)
 	secMuxer := new(csms.SSMuxer)
-	secMuxer.AddTransport(insecure.ID, insecure.NewWithIdentity(id, pk))
+	secMuxer.AddTransport(insecure.ID, insecure.NewWithIdentity(insecure.ID, id, pk))
 
-	stMuxer := msmux.NewBlankTransport()
-	stMuxer.AddTransport("/yamux/1.0.0", yamux.DefaultTransport)
-	u, err := tptu.New(secMuxer, stMuxer)
+	u, err := tptu.New(secMuxer, []tptu.StreamMuxer{{ID: "/yamux/1.0.0", Muxer: yamux.DefaultTransport}}, nil, nil, nil)
 	require.NoError(t, err)
 	return u
 }
diff --git a/p2p/net/swarm/testing/testing.go b/p2p/net/swarm/testing/testing.go
index 604d1d0a..8b9420c4 100644
--- a/p2p/net/swarm/testing/testing.go
+++ b/p2p/net/swarm/testing/testing.go
@@ -15,7 +15,6 @@ import (
 	"github.com/libp2p/go-libp2p/core/sec/insecure"
 	"github.com/libp2p/go-libp2p/core/transport"
 	"github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem"
-	msmux "github.com/libp2p/go-libp2p/p2p/muxer/muxer-multistream"
 	"github.com/libp2p/go-libp2p/p2p/muxer/yamux"
 	csms "github.com/libp2p/go-libp2p/p2p/net/conn-security-multistream"
 	"github.com/libp2p/go-libp2p/p2p/net/swarm"
@@ -99,15 +98,13 @@ func OptPeerPrivateKey(sk crypto.PrivKey) Option {
 }
 
 // GenUpgrader creates a new connection upgrader for use with this swarm.
-func GenUpgrader(t *testing.T, n *swarm.Swarm, opts ...tptu.Option) transport.Upgrader {
+func GenUpgrader(t *testing.T, n *swarm.Swarm, connGater connmgr.ConnectionGater, opts ...tptu.Option) transport.Upgrader {
 	id := n.LocalPeer()
 	pk := n.Peerstore().PrivKey(id)
 	secMuxer := new(csms.SSMuxer)
-	secMuxer.AddTransport(insecure.ID, insecure.NewWithIdentity(id, pk))
+	secMuxer.AddTransport(insecure.ID, insecure.NewWithIdentity(insecure.ID, id, pk))
 
-	stMuxer := msmux.NewBlankTransport()
-	stMuxer.AddTransport("/yamux/1.0.0", yamux.DefaultTransport)
-	u, err := tptu.New(secMuxer, stMuxer, opts...)
+	u, err := tptu.New(secMuxer, []tptu.StreamMuxer{{ID: "/yamux/1.0.0", Muxer: yamux.DefaultTransport}}, nil, nil, connGater, opts...)
 	require.NoError(t, err)
 	return u
 }
@@ -145,7 +142,7 @@ func GenSwarm(t *testing.T, opts ...Option) *swarm.Swarm {
 	s, err := swarm.NewSwarm(id, ps, swarmOpts...)
 	require.NoError(t, err)
 
-	upgrader := GenUpgrader(t, s, tptu.WithConnectionGater(cfg.connectionGater))
+	upgrader := GenUpgrader(t, s, cfg.connectionGater)
 
 	if !cfg.disableTCP {
 		var tcpOpts []tcp.Option
diff --git a/p2p/net/swarm/testing/testing_test.go b/p2p/net/swarm/testing/testing_test.go
index ef625702..d4a43dfb 100644
--- a/p2p/net/swarm/testing/testing_test.go
+++ b/p2p/net/swarm/testing/testing_test.go
@@ -9,5 +9,5 @@ import (
 func TestGenSwarm(t *testing.T) {
 	swarm := GenSwarm(t)
 	require.NoError(t, swarm.Close())
-	GenUpgrader(t, swarm)
+	GenUpgrader(t, swarm, nil)
 }
diff --git a/p2p/net/upgrader/listener_test.go b/p2p/net/upgrader/listener_test.go
index fcb7bf9c..5b541075 100644
--- a/p2p/net/upgrader/listener_test.go
+++ b/p2p/net/upgrader/listener_test.go
@@ -100,7 +100,7 @@ func TestConnectionsClosedIfNotAccepted(t *testing.T) {
 		timeout = 500 * time.Millisecond
 	}
 
-	id, u := createUpgrader(t, upgrader.WithAcceptTimeout(timeout))
+	id, u := createUpgraderWithOpts(t, upgrader.WithAcceptTimeout(timeout))
 	ln := createListener(t, u)
 	defer ln.Close()
 
@@ -134,7 +134,7 @@ func TestConnectionsClosedIfNotAccepted(t *testing.T) {
 func TestFailedUpgradeOnListen(t *testing.T) {
 	require := require.New(t)
 
-	id, u := createUpgraderWithMuxer(t, &errorMuxer{})
+	id, u := createUpgraderWithMuxers(t, []upgrader.StreamMuxer{{ID: "errorMuxer", Muxer: &errorMuxer{}}}, nil, nil)
 	ln := createListener(t, u)
 
 	errCh := make(chan error)
@@ -225,7 +225,7 @@ func TestConcurrentAccept(t *testing.T) {
 	var num = 3 * upgrader.AcceptQueueLength
 
 	blockingMuxer := newBlockingMuxer()
-	id, u := createUpgraderWithMuxer(t, blockingMuxer)
+	id, u := createUpgraderWithMuxers(t, []upgrader.StreamMuxer{{ID: "blockingMuxer", Muxer: blockingMuxer}}, nil, nil)
 	ln := createListener(t, u)
 	defer ln.Close()
 
@@ -309,7 +309,7 @@ func TestListenerConnectionGater(t *testing.T) {
 	require := require.New(t)
 
 	testGater := &testGater{}
-	id, u := createUpgrader(t, upgrader.WithConnectionGater(testGater))
+	id, u := createUpgraderWithConnGater(t, testGater)
 
 	ln := createListener(t, u)
 	defer ln.Close()
@@ -354,7 +354,7 @@ func TestListenerResourceManagement(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	rcmgr := mocknetwork.NewMockResourceManager(ctrl)
-	id, upgrader := createUpgrader(t, upgrader.WithResourceManager(rcmgr))
+	id, upgrader := createUpgraderWithResourceManager(t, rcmgr)
 	ln := createListener(t, upgrader)
 	defer ln.Close()
 
@@ -380,7 +380,7 @@ func TestListenerResourceManagementDenied(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	rcmgr := mocknetwork.NewMockResourceManager(ctrl)
-	id, upgrader := createUpgrader(t, upgrader.WithResourceManager(rcmgr))
+	id, upgrader := createUpgraderWithResourceManager(t, rcmgr)
 	ln := createListener(t, upgrader)
 
 	rcmgr.EXPECT().OpenConnection(network.DirInbound, true, gomock.Not(ln.Multiaddr())).Return(nil, errors.New("nope"))
diff --git a/p2p/net/upgrader/upgrader_test.go b/p2p/net/upgrader/upgrader_test.go
index 16188bd5..106752ab 100644
--- a/p2p/net/upgrader/upgrader_test.go
+++ b/p2p/net/upgrader/upgrader_test.go
@@ -6,6 +6,7 @@ import (
 	"net"
 	"testing"
 
+	"github.com/libp2p/go-libp2p/core/connmgr"
 	"github.com/libp2p/go-libp2p/core/crypto"
 	"github.com/libp2p/go-libp2p/core/network"
 	mocknetwork "github.com/libp2p/go-libp2p/core/network/mocks"
@@ -22,16 +23,28 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func createUpgrader(t *testing.T, opts ...upgrader.Option) (peer.ID, transport.Upgrader) {
-	return createUpgraderWithMuxer(t, &negotiatingMuxer{}, opts...)
+func createUpgrader(t *testing.T) (peer.ID, transport.Upgrader) {
+	return createUpgraderWithMuxers(t, []upgrader.StreamMuxer{{ID: "negotiate", Muxer: &negotiatingMuxer{}}}, nil, nil)
 }
 
-func createUpgraderWithMuxer(t *testing.T, muxer network.Multiplexer, opts ...upgrader.Option) (peer.ID, transport.Upgrader) {
+func createUpgraderWithConnGater(t *testing.T, connGater connmgr.ConnectionGater) (peer.ID, transport.Upgrader) {
+	return createUpgraderWithMuxers(t, []upgrader.StreamMuxer{{ID: "negotiate", Muxer: &negotiatingMuxer{}}}, nil, connGater)
+}
+
+func createUpgraderWithResourceManager(t *testing.T, rcmgr network.ResourceManager) (peer.ID, transport.Upgrader) {
+	return createUpgraderWithMuxers(t, []upgrader.StreamMuxer{{ID: "negotiate", Muxer: &negotiatingMuxer{}}}, rcmgr, nil)
+}
+
+func createUpgraderWithOpts(t *testing.T, opts ...upgrader.Option) (peer.ID, transport.Upgrader) {
+	return createUpgraderWithMuxers(t, []upgrader.StreamMuxer{{ID: "negotiate", Muxer: &negotiatingMuxer{}}}, nil, nil, opts...)
+}
+
+func createUpgraderWithMuxers(t *testing.T, muxers []upgrader.StreamMuxer, rcmgr network.ResourceManager, connGater connmgr.ConnectionGater, opts ...upgrader.Option) (peer.ID, transport.Upgrader) {
 	priv, _, err := test.RandTestKeyPair(crypto.Ed25519, 256)
 	require.NoError(t, err)
 	id, err := peer.IDFromPrivateKey(priv)
 	require.NoError(t, err)
-	u, err := upgrader.New(&MuxAdapter{tpt: insecure.NewWithIdentity(id, priv)}, muxer, opts...)
+	u, err := upgrader.New(&MuxAdapter{tpt: insecure.NewWithIdentity(insecure.ID, id, priv)}, muxers, nil, rcmgr, connGater, opts...)
 	require.NoError(t, err)
 	return id, u
 }
@@ -54,7 +67,7 @@ func (m *negotiatingMuxer) NewConn(c net.Conn, isServer bool, scope network.Peer
 	return yamux.DefaultTransport.NewConn(c, isServer, scope)
 }
 
-// blockingMuxer blocks the muxer negotiation until the contain chan is closed
+// blockingMuxer blocks the muxer negotiation until the contained chan is closed
 type blockingMuxer struct {
 	unblock chan struct{}
 }
@@ -120,7 +133,7 @@ func TestOutboundConnectionGating(t *testing.T) {
 	defer ln.Close()
 
 	testGater := &testGater{}
-	_, dialUpgrader := createUpgrader(t, upgrader.WithConnectionGater(testGater))
+	_, dialUpgrader := createUpgraderWithConnGater(t, testGater)
 	conn, err := dial(t, dialUpgrader, ln.Multiaddr(), id, &network.NullScope{})
 	require.NoError(err)
 	require.NotNil(conn)
@@ -164,7 +177,7 @@ func TestOutboundResourceManagement(t *testing.T) {
 	})
 
 	t.Run("failed negotiation", func(t *testing.T) {
-		id, upgrader := createUpgraderWithMuxer(t, &errorMuxer{})
+		id, upgrader := createUpgraderWithMuxers(t, []upgrader.StreamMuxer{{ID: "errorMuxer", Muxer: &errorMuxer{}}}, nil, nil)
 		ln := createListener(t, upgrader)
 		defer ln.Close()
 
diff --git a/p2p/protocol/circuitv2/relay/relay_test.go b/p2p/protocol/circuitv2/relay/relay_test.go
index 33a85016..fc797c42 100644
--- a/p2p/protocol/circuitv2/relay/relay_test.go
+++ b/p2p/protocol/circuitv2/relay/relay_test.go
@@ -53,7 +53,7 @@ func getNetHosts(t *testing.T, ctx context.Context, n int) (hosts []host.Host, u
 			t.Fatal(err)
 		}
 
-		upgrader := swarmt.GenUpgrader(t, netw)
+		upgrader := swarmt.GenUpgrader(t, netw, nil)
 		upgraders = append(upgraders, upgrader)
 
 		tpt, err := tcp.NewTCPTransport(upgrader, nil)
diff --git a/p2p/protocol/holepunch/holepunch_test.go b/p2p/protocol/holepunch/holepunch_test.go
index 5c364ff0..002ae5cd 100644
--- a/p2p/protocol/holepunch/holepunch_test.go
+++ b/p2p/protocol/holepunch/holepunch_test.go
@@ -45,6 +45,21 @@ func (m *mockEventTracer) getEvents() []*holepunch.Event {
 
 var _ holepunch.EventTracer = &mockEventTracer{}
 
+type mockMaddrFilter struct {
+	filterLocal  func(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr
+	filterRemote func(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr
+}
+
+func (m mockMaddrFilter) FilterLocal(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr {
+	return m.filterLocal(remoteID, maddrs)
+}
+
+func (m mockMaddrFilter) FilterRemote(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr {
+	return m.filterRemote(remoteID, maddrs)
+}
+
+var _ holepunch.AddrFilter = &mockMaddrFilter{}
+
 type mockIDService struct {
 	identify.IDService
 }
@@ -110,7 +125,7 @@ func TestDirectDialWorks(t *testing.T) {
 func TestEndToEndSimConnect(t *testing.T) {
 	h1tr := &mockEventTracer{}
 	h2tr := &mockEventTracer{}
-	h1, h2, relay, _ := makeRelayedHosts(t, holepunch.WithTracer(h1tr), holepunch.WithTracer(h2tr), true)
+	h1, h2, relay, _ := makeRelayedHosts(t, []holepunch.Option{holepunch.WithTracer(h1tr)}, []holepunch.Option{holepunch.WithTracer(h2tr)}, true)
 	defer h1.Close()
 	defer h2.Close()
 	defer relay.Close()
@@ -151,6 +166,7 @@ func TestFailuresOnInitiator(t *testing.T) {
 		rhandler         func(s network.Stream)
 		errMsg           string
 		holePunchTimeout time.Duration
+		filter           func(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr
 	}{
 		"responder does NOT send a CONNECT message": {
 			rhandler: func(s network.Stream) {
@@ -175,6 +191,12 @@ func TestFailuresOnInitiator(t *testing.T) {
 			},
 			errMsg: "i/o deadline reached",
 		},
+		"no addrs after filtering": {
+			errMsg: "aborting hole punch initiation as we have no public address",
+			filter: func(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr {
+				return []ma.Multiaddr{}
+			},
+		},
 	}
 
 	for name, tc := range tcs {
@@ -190,7 +212,22 @@ func TestFailuresOnInitiator(t *testing.T) {
 			defer h1.Close()
 			defer h2.Close()
 			defer relay.Close()
-			hps := addHolePunchService(t, h2, holepunch.WithTracer(tr))
+
+			opts := []holepunch.Option{holepunch.WithTracer(tr)}
+			if tc.filter != nil {
+				f := mockMaddrFilter{
+					filterLocal:  tc.filter,
+					filterRemote: tc.filter,
+				}
+				opts = append(opts, holepunch.WithAddrFilter(f))
+			}
+
+			hps := addHolePunchService(t, h2, opts...)
+			// wait until the hole punching protocol has actually started
+			require.Eventually(t, func() bool {
+				protos, _ := h2.Peerstore().SupportsProtocols(h1.ID(), string(holepunch.Protocol))
+				return len(protos) > 0
+			}, 200*time.Millisecond, 10*time.Millisecond)
 
 			if tc.rhandler != nil {
 				h1.SetStreamHandler(holepunch.Protocol, tc.rhandler)
@@ -221,6 +258,7 @@ func TestFailuresOnResponder(t *testing.T) {
 		initiator        func(s network.Stream)
 		errMsg           string
 		holePunchTimeout time.Duration
+		filter           func(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr
 	}{
 		"initiator does NOT send a CONNECT message": {
 			initiator: func(s network.Stream) {
@@ -258,6 +296,19 @@ func TestFailuresOnResponder(t *testing.T) {
 			},
 			errMsg: "expected CONNECT message to contain at least one address",
 		},
+		"no addrs after filtering": {
+			errMsg: "rejecting hole punch request, as we don't have any public addresses",
+			initiator: func(s network.Stream) {
+				protoio.NewDelimitedWriter(s).WriteMsg(&holepunch_pb.HolePunch{
+					Type:     holepunch_pb.HolePunch_CONNECT.Enum(),
+					ObsAddrs: addrsToBytes([]ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/1234")}),
+				})
+				time.Sleep(10 * time.Second)
+			},
+			filter: func(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr {
+				return []ma.Multiaddr{}
+			},
+		},
 	}
 
 	for name, tc := range tcs {
@@ -267,9 +318,18 @@ func TestFailuresOnResponder(t *testing.T) {
 				holepunch.StreamTimeout = tc.holePunchTimeout
 				defer func() { holepunch.StreamTimeout = cpy }()
 			}
-
 			tr := &mockEventTracer{}
-			h1, h2, relay, _ := makeRelayedHosts(t, holepunch.WithTracer(tr), nil, false)
+
+			opts := []holepunch.Option{holepunch.WithTracer(tr)}
+			if tc.filter != nil {
+				f := mockMaddrFilter{
+					filterLocal:  tc.filter,
+					filterRemote: tc.filter,
+				}
+				opts = append(opts, holepunch.WithAddrFilter(f))
+			}
+
+			h1, h2, relay, _ := makeRelayedHosts(t, opts, nil, false)
 			defer h1.Close()
 			defer h2.Close()
 			defer relay.Close()
@@ -359,9 +419,11 @@ func mkHostWithStaticAutoRelay(t *testing.T, relay host.Host) host.Host {
 	h, err := libp2p.New(
 		libp2p.ListenAddrs(ma.StringCast("/ip4/127.0.0.1/tcp/0")),
 		libp2p.EnableRelay(),
-		libp2p.EnableAutoRelay(autorelay.WithCircuitV1Support()),
+		libp2p.EnableAutoRelay(
+			autorelay.WithCircuitV1Support(),
+			autorelay.WithStaticRelays([]peer.AddrInfo{pi}),
+		),
 		libp2p.ForceReachabilityPrivate(),
-		libp2p.StaticRelays([]peer.AddrInfo{pi}),
 	)
 	require.NoError(t, err)
 
@@ -377,13 +439,9 @@ func mkHostWithStaticAutoRelay(t *testing.T, relay host.Host) host.Host {
 	return h
 }
 
-func makeRelayedHosts(t *testing.T, h1opt, h2opt holepunch.Option, addHolePuncher bool) (h1, h2, relay host.Host, hps *holepunch.Service) {
+func makeRelayedHosts(t *testing.T, h1opt, h2opt []holepunch.Option, addHolePuncher bool) (h1, h2, relay host.Host, hps *holepunch.Service) {
 	t.Helper()
-	var h1opts []holepunch.Option
-	if h1opt != nil {
-		h1opts = append(h1opts, h1opt)
-	}
-	h1, _ = mkHostWithHolePunchSvc(t, h1opts...)
+	h1, _ = mkHostWithHolePunchSvc(t, h1opt...)
 	var err error
 	relay, err = libp2p.New(libp2p.ListenAddrs(ma.StringCast("/ip4/127.0.0.1/tcp/0")), libp2p.DisableRelay())
 	require.NoError(t, err)
@@ -393,7 +451,7 @@ func makeRelayedHosts(t *testing.T, h1opt, h2opt holepunch.Option, addHolePunche
 
 	h2 = mkHostWithStaticAutoRelay(t, relay)
 	if addHolePuncher {
-		hps = addHolePunchService(t, h2, h2opt)
+		hps = addHolePunchService(t, h2, h2opt...)
 	}
 
 	// h1 has a relay addr
@@ -413,12 +471,8 @@ func makeRelayedHosts(t *testing.T, h1opt, h2opt holepunch.Option, addHolePunche
 	return
 }
 
-func addHolePunchService(t *testing.T, h host.Host, opt holepunch.Option) *holepunch.Service {
+func addHolePunchService(t *testing.T, h host.Host, opts ...holepunch.Option) *holepunch.Service {
 	t.Helper()
-	var opts []holepunch.Option
-	if opt != nil {
-		opts = append(opts, opt)
-	}
 	hps, err := holepunch.NewService(h, newMockIDService(t, h), opts...)
 	require.NoError(t, err)
 	return hps
diff --git a/p2p/protocol/internal/circuitv1-deprecated/relay_test.go b/p2p/protocol/internal/circuitv1-deprecated/relay_test.go
index 9b76cffc..30a80f4f 100644
--- a/p2p/protocol/internal/circuitv1-deprecated/relay_test.go
+++ b/p2p/protocol/internal/circuitv1-deprecated/relay_test.go
@@ -44,7 +44,7 @@ func getNetHosts(t *testing.T, n int) []host.Host {
 }
 
 func newTestRelay(t *testing.T, host host.Host, opts ...RelayOpt) *Relay {
-	r, err := NewRelay(host, swarmt.GenUpgrader(t, host.Network().(*swarm.Swarm)), opts...)
+	r, err := NewRelay(host, swarmt.GenUpgrader(t, host.Network().(*swarm.Swarm), nil), opts...)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/p2p/protocol/internal/circuitv1-deprecated/transport_test.go b/p2p/protocol/internal/circuitv1-deprecated/transport_test.go
index 02b37161..c1cb1a9c 100644
--- a/p2p/protocol/internal/circuitv1-deprecated/transport_test.go
+++ b/p2p/protocol/internal/circuitv1-deprecated/transport_test.go
@@ -27,17 +27,17 @@ var msg = []byte("relay works!")
 func testSetupRelay(t *testing.T) []host.Host {
 	hosts := getNetHosts(t, 3)
 
-	err := AddRelayTransport(hosts[0], swarmt.GenUpgrader(t, hosts[0].Network().(*swarm.Swarm)))
+	err := AddRelayTransport(hosts[0], swarmt.GenUpgrader(t, hosts[0].Network().(*swarm.Swarm), nil))
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	err = AddRelayTransport(hosts[1], swarmt.GenUpgrader(t, hosts[1].Network().(*swarm.Swarm)), OptHop)
+	err = AddRelayTransport(hosts[1], swarmt.GenUpgrader(t, hosts[1].Network().(*swarm.Swarm), nil), OptHop)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	err = AddRelayTransport(hosts[2], swarmt.GenUpgrader(t, hosts[2].Network().(*swarm.Swarm)))
+	err = AddRelayTransport(hosts[2], swarmt.GenUpgrader(t, hosts[2].Network().(*swarm.Swarm), nil))
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/p2p/security/noise/benchmark_test.go b/p2p/security/noise/benchmark_test.go
index 836275b9..d59a1cb9 100644
--- a/p2p/security/noise/benchmark_test.go
+++ b/p2p/security/noise/benchmark_test.go
@@ -39,7 +39,7 @@ func makeTransport(b *testing.B) *Transport {
 	if err != nil {
 		b.Fatal(err)
 	}
-	tpt, err := New(priv, nil)
+	tpt, err := New(ID, priv, nil)
 	if err != nil {
 		b.Fatalf("error constructing transport: %v", err)
 	}
diff --git a/p2p/security/noise/crypto_test.go b/p2p/security/noise/crypto_test.go
index 1ca11476..5c40a5ef 100644
--- a/p2p/security/noise/crypto_test.go
+++ b/p2p/security/noise/crypto_test.go
@@ -93,7 +93,7 @@ func TestCryptoFailsIfHandshakeIncomplete(t *testing.T) {
 	init, resp := net.Pipe()
 	_ = resp.Close()
 
-	session, _ := newSecureSession(initTransport, context.TODO(), init, "remote-peer", nil, nil, nil, true)
+	session, _ := newSecureSession(initTransport, context.TODO(), init, "remote-peer", nil, nil, nil, true, true)
 	_, err := session.encrypt(nil, []byte("hi"))
 	if err == nil {
 		t.Error("expected encryption error when handshake incomplete")
diff --git a/p2p/security/noise/transport_test.go b/p2p/security/noise/transport_test.go
index c3b18047..056afa39 100644
--- a/p2p/security/noise/transport_test.go
+++ b/p2p/security/noise/transport_test.go
@@ -232,6 +232,44 @@ func TestPeerIDMismatchInboundFailsHandshake(t *testing.T) {
 	<-done
 }
 
+func TestPeerIDInboundCheckDisabled(t *testing.T) {
+	initTransport := newTestTransport(t, crypto.Ed25519, 2048)
+	respTransport := newTestTransport(t, crypto.Ed25519, 2048)
+	init, resp := newConnPair(t)
+
+	initSessionTransport, err := initTransport.WithSessionOptions(DisablePeerIDCheck())
+	require.NoError(t, err)
+	errChan := make(chan error)
+	go func() {
+		_, err := initSessionTransport.SecureInbound(context.Background(), init, "test")
+		errChan <- err
+	}()
+	_, err = respTransport.SecureOutbound(context.Background(), resp, initTransport.localID)
+	require.NoError(t, err)
+	initErr := <-errChan
+	require.NoError(t, initErr)
+}
+
+func TestPeerIDOutboundNoCheck(t *testing.T) {
+	initTransport := newTestTransport(t, crypto.Ed25519, 2048)
+	respTransport := newTestTransport(t, crypto.Ed25519, 2048)
+	init, resp := newConnPair(t)
+
+	initSessionTransport, err := initTransport.WithSessionOptions(DisablePeerIDCheck())
+	require.NoError(t, err)
+
+	errChan := make(chan error)
+	go func() {
+		_, err := initSessionTransport.SecureOutbound(context.Background(), init, "test")
+		errChan <- err
+	}()
+
+	_, err = respTransport.SecureInbound(context.Background(), resp, "")
+	require.NoError(t, err)
+	initErr := <-errChan
+	require.NoError(t, initErr)
+}
+
 func makeLargePlaintext(size int) []byte {
 	buf := make([]byte, size)
 	rand.Read(buf)
@@ -594,21 +632,53 @@ func TestEarlyfffDataAcceptedWithNoHandler(t *testing.T) {
 }
 
 type noiseEarlyDataTestCase struct {
-	initProtos     []string
-	respProtos     []string
+	clientProtos   []string
+	serverProtos   []string
 	expectedResult string
 }
 
 func TestHandshakeWithTransportEarlyData(t *testing.T) {
 	tests := []noiseEarlyDataTestCase{
-		{initProtos: nil, respProtos: nil, expectedResult: ""},
-		{[]string{"muxer1"}, []string{"muxer1"}, "muxer1"},
-		{[]string{"muxer1"}, []string{}, ""},
-		{[]string{}, []string{"muxer2"}, ""},
-		{[]string{"muxer2"}, []string{"muxer1"}, ""},
-		{[]string{"muxer1/1.0.0", "muxer2/1.0.1"}, []string{"muxer2/1.0.1", "muxer1/1.0.0"}, "muxer2/1.0.1"},
-		{[]string{"muxer1/1.0.0", "muxer2/1.0.1", "muxer3/1.0.0"}, []string{"muxer2/1.0.1", "muxer1/1.0.1", "muxer3/1.0.0"}, "muxer2/1.0.1"},
-		{[]string{"muxer1/1.0.0", "muxer2/1.0.0"}, []string{"muxer3/1.0.0"}, ""},
+		{
+			clientProtos:   nil,
+			serverProtos:   nil,
+			expectedResult: "",
+		},
+		{
+			clientProtos:   []string{"muxer1"},
+			serverProtos:   []string{"muxer1"},
+			expectedResult: "muxer1",
+		},
+		{
+			clientProtos:   []string{"muxer1"},
+			serverProtos:   []string{},
+			expectedResult: "",
+		},
+		{
+			clientProtos:   []string{},
+			serverProtos:   []string{"muxer2"},
+			expectedResult: "",
+		},
+		{
+			clientProtos:   []string{"muxer2"},
+			serverProtos:   []string{"muxer1"},
+			expectedResult: "",
+		},
+		{
+			clientProtos:   []string{"muxer1", "muxer2"},
+			serverProtos:   []string{"muxer2", "muxer1"},
+			expectedResult: "muxer1",
+		},
+		{
+			clientProtos:   []string{"muxer3", "muxer2", "muxer1"},
+			serverProtos:   []string{"muxer2", "muxer1"},
+			expectedResult: "muxer2",
+		},
+		{
+			clientProtos:   []string{"muxer1", "muxer2"},
+			serverProtos:   []string{"muxer3"},
+			expectedResult: "",
+		},
 	}
 
 	noiseHandshake := func(t *testing.T, initProtos, respProtos []string, expectedProto string) {
@@ -624,24 +694,17 @@ func TestHandshakeWithTransportEarlyData(t *testing.T) {
 
 		initData := []byte("Test data for noise transport")
 		_, err := initConn.Write(initData)
-		if err != nil {
-			t.Fatal(err)
-		}
+		require.NoError(t, err)
 
 		respData := make([]byte, len(initData))
 		_, err = respConn.Read(respData)
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		if !bytes.Equal(initData, respData) {
-			t.Errorf("Data transmitted mismatch over noise session. %v != %v", initData, respData)
-		}
+		require.NoError(t, err)
+		require.Equal(t, initData, respData)
 	}
 
 	for _, test := range tests {
 		t.Run("Transport EarlyData Test", func(t *testing.T) {
-			noiseHandshake(t, test.initProtos, test.respProtos, test.expectedResult)
+			noiseHandshake(t, test.clientProtos, test.serverProtos, test.expectedResult)
 		})
 	}
 }
diff --git a/p2p/security/tls/transport_test.go b/p2p/security/tls/transport_test.go
index 59fa8bda..0d42eb1e 100644
--- a/p2p/security/tls/transport_test.go
+++ b/p2p/security/tls/transport_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/libp2p/go-libp2p/core/peer"
 	"github.com/libp2p/go-libp2p/core/protocol"
 	"github.com/libp2p/go-libp2p/core/sec"
+	tptu "github.com/libp2p/go-libp2p/p2p/net/upgrader"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -126,9 +127,9 @@ func TestHandshakeSucceeds(t *testing.T) {
 	}
 
 	// Use standard transports with default TLS configuration
-	clientTransport, err := New(clientKey, nil)
+	clientTransport, err := New(ID, clientKey, nil)
 	require.NoError(t, err)
-	serverTransport, err := New(serverKey, nil)
+	serverTransport, err := New(ID, serverKey, nil)
 	require.NoError(t, err)
 
 	t.Run("standard TLS with extension not critical", func(t *testing.T) {
@@ -183,16 +184,42 @@ type testcase struct {
 }
 
 func TestHandshakeWithNextProtoSucceeds(t *testing.T) {
-
 	tests := []testcase{
-		{clientProtos: nil, serverProtos: nil, expectedResult: ""},
-		{[]protocol.ID{"muxer1/1.0.0", "muxer2/1.0.1"}, []protocol.ID{"muxer2/1.0.1", "muxer1/1.0.0"}, "muxer2/1.0.1"},
-		{[]protocol.ID{"muxer1/1.0.0", "muxer2/1.0.1", "libp2p"}, []protocol.ID{"muxer2/1.0.1", "muxer1/1.0.0", "libp2p"}, "muxer2/1.0.1"},
-		{[]protocol.ID{"muxer1/1.0.0", "libp2p"}, []protocol.ID{"libp2p"}, ""},
-		{[]protocol.ID{"libp2p"}, []protocol.ID{"libp2p"}, ""},
-		{[]protocol.ID{"muxer1"}, []protocol.ID{}, ""},
-		{[]protocol.ID{}, []protocol.ID{"muxer1"}, ""},
-		{[]protocol.ID{"muxer2"}, []protocol.ID{"muxer1"}, ""},
+		{
+			clientProtos:   []protocol.ID{"muxer1", "muxer2"},
+			serverProtos:   []protocol.ID{"muxer2", "muxer1"},
+			expectedResult: "muxer1",
+		},
+		{
+			clientProtos:   []protocol.ID{"muxer1", "muxer2", "libp2p"},
+			serverProtos:   []protocol.ID{"muxer2", "muxer1", "libp2p"},
+			expectedResult: "muxer1",
+		},
+		{
+			clientProtos:   []protocol.ID{"muxer1", "libp2p"},
+			serverProtos:   []protocol.ID{"libp2p"},
+			expectedResult: "",
+		},
+		{
+			clientProtos:   []protocol.ID{"libp2p"},
+			serverProtos:   []protocol.ID{"libp2p"},
+			expectedResult: "",
+		},
+		{
+			clientProtos:   []protocol.ID{"muxer1"},
+			serverProtos:   []protocol.ID{},
+			expectedResult: "",
+		},
+		{
+			clientProtos:   []protocol.ID{},
+			serverProtos:   []protocol.ID{"muxer1"},
+			expectedResult: "",
+		},
+		{
+			clientProtos:   []protocol.ID{"muxer2"},
+			serverProtos:   []protocol.ID{"muxer1"},
+			expectedResult: "",
+		},
 	}
 
 	clientID, clientKey := createPeer(t)
@@ -240,9 +267,17 @@ func TestHandshakeWithNextProtoSucceeds(t *testing.T) {
 
 	// Iterate through the NextProto combinations.
 	for _, test := range tests {
-		clientTransport, err := New(clientKey, test.clientProtos)
+		clientMuxers := make([]tptu.StreamMuxer, 0, len(test.clientProtos))
+		for _, id := range test.clientProtos {
+			clientMuxers = append(clientMuxers, tptu.StreamMuxer{ID: id})
+		}
+		clientTransport, err := New(ID, clientKey, clientMuxers)
 		require.NoError(t, err)
-		serverTransport, err := New(serverKey, test.serverProtos)
+		serverMuxers := make([]tptu.StreamMuxer, 0, len(test.clientProtos))
+		for _, id := range test.serverProtos {
+			serverMuxers = append(serverMuxers, tptu.StreamMuxer{ID: id})
+		}
+		serverTransport, err := New(ID, serverKey, serverMuxers)
 		require.NoError(t, err)
 
 		t.Run("TLS handshake with ALPN extension", func(t *testing.T) {
@@ -268,9 +303,9 @@ func TestHandshakeConnectionCancellations(t *testing.T) {
 	_, clientKey := createPeer(t)
 	serverID, serverKey := createPeer(t)
 
-	clientTransport, err := New(clientKey, nil)
+	clientTransport, err := New(ID, clientKey, nil)
 	require.NoError(t, err)
-	serverTransport, err := New(serverKey, nil)
+	serverTransport, err := New(ID, serverKey, nil)
 	require.NoError(t, err)
 
 	t.Run("cancel outgoing connection", func(t *testing.T) {
@@ -320,9 +355,9 @@ func TestPeerIDMismatch(t *testing.T) {
 	_, clientKey := createPeer(t)
 	serverID, serverKey := createPeer(t)
 
-	serverTransport, err := New(serverKey, nil)
+	serverTransport, err := New(ID, serverKey, nil)
 	require.NoError(t, err)
-	clientTransport, err := New(clientKey, nil)
+	clientTransport, err := New(ID, clientKey, nil)
 	require.NoError(t, err)
 
 	t.Run("for outgoing connections", func(t *testing.T) {
@@ -597,9 +632,9 @@ func TestInvalidCerts(t *testing.T) {
 		tr := transforms[i]
 
 		t.Run(fmt.Sprintf("client offending: %s", tr.name), func(t *testing.T) {
-			serverTransport, err := New(serverKey, nil)
+			serverTransport, err := New(ID, serverKey, nil)
 			require.NoError(t, err)
-			clientTransport, err := New(clientKey, nil)
+			clientTransport, err := New(ID, clientKey, nil)
 			require.NoError(t, err)
 			tr.apply(clientTransport.identity)
 
@@ -640,10 +675,10 @@ func TestInvalidCerts(t *testing.T) {
 		})
 
 		t.Run(fmt.Sprintf("server offending: %s", tr.name), func(t *testing.T) {
-			serverTransport, err := New(serverKey, nil)
+			serverTransport, err := New(ID, serverKey, nil)
 			require.NoError(t, err)
 			tr.apply(serverTransport.identity)
-			clientTransport, err := New(clientKey, nil)
+			clientTransport, err := New(ID, clientKey, nil)
 			require.NoError(t, err)
 
 			clientInsecureConn, serverInsecureConn := connect(t)
diff --git a/p2p/test/muxer-negotiation/muxer_test.go b/p2p/test/muxer-negotiation/muxer_test.go
new file mode 100644
index 00000000..a36bfbde
--- /dev/null
+++ b/p2p/test/muxer-negotiation/muxer_test.go
@@ -0,0 +1,126 @@
+package muxer_negotiation
+
+import (
+	"context"
+	"crypto/rand"
+	"fmt"
+	"testing"
+
+	"github.com/libp2p/go-libp2p"
+	"github.com/libp2p/go-libp2p/core/crypto"
+	"github.com/libp2p/go-libp2p/core/peer"
+	"github.com/libp2p/go-libp2p/core/protocol"
+	"github.com/libp2p/go-libp2p/core/sec/insecure"
+	"github.com/libp2p/go-libp2p/p2p/muxer/mplex"
+	"github.com/libp2p/go-libp2p/p2p/muxer/yamux"
+	"github.com/libp2p/go-libp2p/p2p/security/noise"
+	tls "github.com/libp2p/go-libp2p/p2p/security/tls"
+	"github.com/libp2p/go-libp2p/p2p/transport/tcp"
+
+	"github.com/stretchr/testify/require"
+)
+
+var (
+	yamuxOpt = libp2p.Muxer("/yamux", yamux.DefaultTransport)
+	mplexOpt = libp2p.Muxer("/mplex", mplex.DefaultTransport)
+)
+
+type testcase struct {
+	Name             string
+	ServerPreference []libp2p.Option
+	ClientPreference []libp2p.Option
+
+	Error    string
+	Expected protocol.ID
+}
+
+type security struct {
+	Name   string
+	Option libp2p.Option
+}
+
+func TestMuxerNegotiation(t *testing.T) {
+	testcases := []testcase{
+		{
+			Name:             "server and client have the same preference",
+			ServerPreference: []libp2p.Option{yamuxOpt, mplexOpt},
+			ClientPreference: []libp2p.Option{yamuxOpt, mplexOpt},
+			Expected:         "/yamux",
+		},
+		{
+			Name:             "client only supports one muxer",
+			ServerPreference: []libp2p.Option{yamuxOpt, mplexOpt},
+			ClientPreference: []libp2p.Option{yamuxOpt},
+			Expected:         "/yamux",
+		},
+		{
+			Name:             "server only supports one muxer",
+			ServerPreference: []libp2p.Option{yamuxOpt},
+			ClientPreference: []libp2p.Option{mplexOpt, yamuxOpt},
+			Expected:         "/yamux",
+		},
+		{
+			Name:             "client preference preferred",
+			ServerPreference: []libp2p.Option{yamuxOpt, mplexOpt},
+			ClientPreference: []libp2p.Option{mplexOpt, yamuxOpt},
+			Expected:         "/mplex",
+		},
+		{
+			Name:             "no preference overlap",
+			ServerPreference: []libp2p.Option{yamuxOpt},
+			ClientPreference: []libp2p.Option{mplexOpt},
+			Error:            "failed to negotiate stream multiplexer: protocol not supported",
+		},
+	}
+
+	clientID, _, err := crypto.GenerateEd25519Key(rand.Reader)
+	require.NoError(t, err)
+	serverID, _, err := crypto.GenerateEd25519Key(rand.Reader)
+	require.NoError(t, err)
+
+	securities := []security{
+		{Name: "noise", Option: libp2p.Security("/noise", noise.New)},
+		{Name: "tls", Option: libp2p.Security("/tls", tls.New)},
+		{Name: "insecure", Option: libp2p.Security("/insecure", insecure.NewWithIdentity)},
+	}
+
+	for _, tc := range testcases {
+		tc := tc
+
+		for _, sec := range securities {
+			sec := sec
+
+			t.Run(fmt.Sprintf("%s: %s", sec.Name, tc.Name), func(t *testing.T) {
+				server, err := libp2p.New(
+					libp2p.Identity(serverID),
+					sec.Option,
+					libp2p.ChainOptions(tc.ServerPreference...),
+					libp2p.Transport(tcp.NewTCPTransport),
+					libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"),
+				)
+				require.NoError(t, err)
+
+				client, err := libp2p.New(
+					libp2p.Identity(clientID),
+					sec.Option,
+					libp2p.ChainOptions(tc.ClientPreference...),
+					libp2p.Transport(tcp.NewTCPTransport),
+					libp2p.NoListenAddrs,
+				)
+				require.NoError(t, err)
+
+				err = client.Connect(context.Background(), peer.AddrInfo{ID: server.ID(), Addrs: server.Addrs()})
+				if tc.Error != "" {
+					require.Error(t, err)
+					require.ErrorContains(t, err, tc.Error)
+					return
+				}
+
+				require.NoError(t, err)
+				conns := client.Network().ConnsToPeer(server.ID())
+				require.Len(t, conns, 1, "expected exactly one connection")
+				require.Equal(t, tc.Expected, protocol.ID(conns[0].ConnState().NextProto))
+			})
+		}
+	}
+}
diff --git a/p2p/test/webtransport/webtransport_test.go b/p2p/test/webtransport/webtransport_test.go
new file mode 100644
index 00000000..adc64b49
--- /dev/null
+++ b/p2p/test/webtransport/webtransport_test.go
@@ -0,0 +1,53 @@
+package webtransport_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/benbjohnson/clock"
+	"github.com/libp2p/go-libp2p"
+	ic "github.com/libp2p/go-libp2p/core/crypto"
+	"github.com/libp2p/go-libp2p/core/test"
+	libp2pwebtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
+	ma "github.com/multiformats/go-multiaddr"
+	"github.com/stretchr/testify/require"
+)
+
+func extractCertHashes(addr ma.Multiaddr) []string {
+	var certHashesStr []string
+	ma.ForEach(addr, func(c ma.Component) bool {
+		if c.Protocol().Code == ma.P_CERTHASH {
+			certHashesStr = append(certHashesStr, c.Value())
+		}
+		return true
+	})
+	return certHashesStr
+}
+
+func TestDeterministicCertsAfterReboot(t *testing.T) {
+	priv, _, err := test.RandTestKeyPair(ic.Ed25519, 256)
+	require.NoError(t, err)
+
+	cl := clock.NewMock()
+	// Move one year ahead to avoid edge cases around epoch
+	cl.Add(time.Hour * 24 * 365)
+	h, err := libp2p.New(libp2p.NoTransports, libp2p.Transport(libp2pwebtransport.New, libp2pwebtransport.WithClock(cl)), libp2p.Identity(priv))
+	require.NoError(t, err)
+	err = h.Network().Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	require.NoError(t, err)
+
+	prevCerthashes := extractCertHashes(h.Addrs()[0])
+	h.Close()
+
+	h, err = libp2p.New(libp2p.NoTransports, libp2p.Transport(libp2pwebtransport.New, libp2pwebtransport.WithClock(cl)), libp2p.Identity(priv))
+	require.NoError(t, err)
+	defer h.Close()
+	err = h.Network().Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	require.NoError(t, err)
+
+	nextCertHashes := extractCertHashes(h.Addrs()[0])
+
+	for i := range prevCerthashes {
+		require.Equal(t, prevCerthashes[i], nextCertHashes[i])
+	}
+}
diff --git a/p2p/transport/tcp/tcp_test.go b/p2p/transport/tcp/tcp_test.go
index 3d49a253..92040463 100644
--- a/p2p/transport/tcp/tcp_test.go
+++ b/p2p/transport/tcp/tcp_test.go
@@ -22,16 +22,18 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+var muxers = []tptu.StreamMuxer{{ID: "/yamux", Muxer: yamux.DefaultTransport}}
+
 func TestTcpTransport(t *testing.T) {
 	for i := 0; i < 2; i++ {
 		peerA, ia := makeInsecureMuxer(t)
 		_, ib := makeInsecureMuxer(t)
 
-		ua, err := tptu.New(ia, yamux.DefaultTransport)
+		ua, err := tptu.New(ia, muxers, nil, nil, nil)
 		require.NoError(t, err)
 		ta, err := NewTCPTransport(ua, nil)
 		require.NoError(t, err)
-		ub, err := tptu.New(ib, yamux.DefaultTransport)
+		ub, err := tptu.New(ib, muxers, nil, nil, nil)
 		require.NoError(t, err)
 		tb, err := NewTCPTransport(ub, nil)
 		require.NoError(t, err)
@@ -48,11 +50,11 @@ func TestTcpTransportWithMetrics(t *testing.T) {
 	peerA, ia := makeInsecureMuxer(t)
 	_, ib := makeInsecureMuxer(t)
 
-	ua, err := tptu.New(ia, yamux.DefaultTransport)
+	ua, err := tptu.New(ia, muxers, nil, nil, nil)
 	require.NoError(t, err)
 	ta, err := NewTCPTransport(ua, nil, WithMetrics())
 	require.NoError(t, err)
-	ub, err := tptu.New(ib, yamux.DefaultTransport)
+	ub, err := tptu.New(ib, muxers, nil, nil, nil)
 	require.NoError(t, err)
 	tb, err := NewTCPTransport(ub, nil, WithMetrics())
 	require.NoError(t, err)
@@ -68,7 +70,7 @@ func TestResourceManager(t *testing.T) {
 	peerA, ia := makeInsecureMuxer(t)
 	_, ib := makeInsecureMuxer(t)
 
-	ua, err := tptu.New(ia, yamux.DefaultTransport)
+	ua, err := tptu.New(ia, muxers, nil, nil, nil)
 	require.NoError(t, err)
 	ta, err := NewTCPTransport(ua, nil)
 	require.NoError(t, err)
@@ -76,7 +78,7 @@ func TestResourceManager(t *testing.T) {
 	require.NoError(t, err)
 	defer ln.Close()
 
-	ub, err := tptu.New(ib, yamux.DefaultTransport)
+	ub, err := tptu.New(ib, muxers, nil, nil, nil)
 	require.NoError(t, err)
 	rcmgr := mocknetwork.NewMockResourceManager(ctrl)
 	tb, err := NewTCPTransport(ub, rcmgr)
@@ -153,6 +155,6 @@ func makeInsecureMuxer(t *testing.T) (peer.ID, sec.SecureMuxer) {
 	id, err := peer.IDFromPrivateKey(priv)
 	require.NoError(t, err)
 	var secMuxer csms.SSMuxer
-	secMuxer.AddTransport(insecure.ID, insecure.NewWithIdentity(id, priv))
+	secMuxer.AddTransport(insecure.ID, insecure.NewWithIdentity(insecure.ID, id, priv))
 	return id, &secMuxer
 }
diff --git a/p2p/transport/websocket/websocket_test.go b/p2p/transport/websocket/websocket_test.go
index 645dc824..016dbe59 100644
--- a/p2p/transport/websocket/websocket_test.go
+++ b/p2p/transport/websocket/websocket_test.go
@@ -39,7 +39,7 @@ import (
 func newUpgrader(t *testing.T) (peer.ID, transport.Upgrader) {
 	t.Helper()
 	id, m := newInsecureMuxer(t)
-	u, err := tptu.New(m, yamux.DefaultTransport)
+	u, err := tptu.New(m, []tptu.StreamMuxer{{ID: "/yamux", Muxer: yamux.DefaultTransport}}, nil, nil, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -49,7 +49,7 @@ func newUpgrader(t *testing.T) (peer.ID, transport.Upgrader) {
 func newSecureUpgrader(t *testing.T) (peer.ID, transport.Upgrader) {
 	t.Helper()
 	id, m := newSecureMuxer(t)
-	u, err := tptu.New(m, yamux.DefaultTransport)
+	u, err := tptu.New(m, []tptu.StreamMuxer{{ID: "/yamux", Muxer: yamux.DefaultTransport}}, nil, nil, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -67,7 +67,7 @@ func newInsecureMuxer(t *testing.T) (peer.ID, sec.SecureMuxer) {
 		t.Fatal(err)
 	}
 	var secMuxer csms.SSMuxer
-	secMuxer.AddTransport(insecure.ID, insecure.NewWithIdentity(id, priv))
+	secMuxer.AddTransport(insecure.ID, insecure.NewWithIdentity(insecure.ID, id, priv))
 	return id, &secMuxer
 }
 
@@ -82,7 +82,7 @@ func newSecureMuxer(t *testing.T) (peer.ID, sec.SecureMuxer) {
 		t.Fatal(err)
 	}
 	var secMuxer csms.SSMuxer
-	noiseTpt, err := noise.New(priv, nil)
+	noiseTpt, err := noise.New(noise.ID, priv, nil)
 	require.NoError(t, err)
 	secMuxer.AddTransport(noise.ID, noiseTpt)
 	return id, &secMuxer
diff --git a/p2p/transport/webtransport/cert_manager_test.go b/p2p/transport/webtransport/cert_manager_test.go
index 3f2328fb..4b4550bb 100644
--- a/p2p/transport/webtransport/cert_manager_test.go
+++ b/p2p/transport/webtransport/cert_manager_test.go
@@ -3,10 +3,14 @@ package libp2pwebtransport
 import (
 	"crypto/sha256"
 	"crypto/tls"
+	"fmt"
 	"testing"
+	"testing/quick"
 	"time"
 
 	"github.com/benbjohnson/clock"
+	"github.com/libp2p/go-libp2p/core/crypto"
+	"github.com/libp2p/go-libp2p/core/test"
 	ma "github.com/multiformats/go-multiaddr"
 	"github.com/multiformats/go-multibase"
 	"github.com/multiformats/go-multihash"
@@ -39,14 +43,16 @@ func certHashFromComponent(t *testing.T, comp ma.Component) []byte {
 func TestInitialCert(t *testing.T) {
 	cl := clock.NewMock()
 	cl.Add(1234567 * time.Hour)
-	m, err := newCertManager(cl)
+	priv, _, err := test.RandTestKeyPair(crypto.Ed25519, 256)
+	require.NoError(t, err)
+	m, err := newCertManager(priv, cl)
 	require.NoError(t, err)
 	defer m.Close()
 
 	conf := m.GetConfig()
 	require.Len(t, conf.Certificates, 1)
 	cert := conf.Certificates[0]
-	require.Equal(t, cl.Now().Add(-clockSkewAllowance).UTC(), cert.Leaf.NotBefore)
+	require.GreaterOrEqual(t, cl.Now().Add(-clockSkewAllowance), cert.Leaf.NotBefore)
 	require.Equal(t, cert.Leaf.NotBefore.Add(certValidity), cert.Leaf.NotAfter)
 	addr := m.AddrComponent()
 	components := splitMultiaddr(addr)
@@ -59,7 +65,11 @@ func TestInitialCert(t *testing.T) {
 
 func TestCertRenewal(t *testing.T) {
 	cl := clock.NewMock()
-	m, err := newCertManager(cl)
+	// Add a year to avoid edge cases around the epoch
+	cl.Add(time.Hour * 24 * 365)
+	priv, _, err := test.SeededTestKeyPair(crypto.Ed25519, 256, 0)
+	require.NoError(t, err)
+	m, err := newCertManager(priv, cl)
 	require.NoError(t, err)
 	defer m.Close()
 
@@ -68,7 +78,7 @@ func TestCertRenewal(t *testing.T) {
 	require.Len(t, first, 2)
 	require.NotEqual(t, first[0].Value(), first[1].Value(), "the hashes should differ")
 	// wait for a new certificate to be generated
-	cl.Add(certValidity - 2*clockSkewAllowance - time.Second)
+	cl.Set(m.currentConfig.End().Add(-(clockSkewAllowance + time.Second)))
 	require.Never(t, func() bool {
 		for i, c := range splitMultiaddr(m.AddrComponent()) {
 			if c.Value() != first[i].Value() {
@@ -100,3 +110,67 @@ func TestCertRenewal(t *testing.T) {
 	// check that the 2nd certificate from the beginning was rolled over to be the 1st certificate
 	require.Equal(t, second[1].Value(), third[0].Value())
 }
+
+func TestDeterministicCertsAcrossReboots(t *testing.T) {
+	// Run this test 100 times to make sure it's deterministic
+	runs := 100
+	for i := 0; i < runs; i++ {
+		t.Run(fmt.Sprintf("Run=%d", i), func(t *testing.T) {
+			cl := clock.NewMock()
+			priv, _, err := test.SeededTestKeyPair(crypto.Ed25519, 256, 0)
+			require.NoError(t, err)
+			m, err := newCertManager(priv, cl)
+			require.NoError(t, err)
+			defer m.Close()
+
+			conf := m.GetConfig()
+			require.Len(t, conf.Certificates, 1)
+			oldCerts := m.serializedCertHashes
+
+			m.Close()
+
+			cl.Add(time.Hour)
+			// reboot
+			m, err = newCertManager(priv, cl)
+			require.NoError(t, err)
+			defer m.Close()
+
+			newCerts := m.serializedCertHashes
+
+			require.Equal(t, oldCerts, newCerts)
+		})
+	}
+}
+
+func TestDeterministicTimeBuckets(t *testing.T) {
+	cl := clock.NewMock()
+	cl.Add(time.Hour * 24 * 365)
+	startA := getCurrentBucketStartTime(cl.Now(), 0)
+	startB := getCurrentBucketStartTime(cl.Now().Add(time.Hour*24), 0)
+	require.Equal(t, startA, startB)
+
+	// 15 Days later
+	startC := getCurrentBucketStartTime(cl.Now().Add(time.Hour*24*15), 0)
+	require.NotEqual(t, startC, startB)
+}
+
+func TestGetCurrentBucketStartTimeIsWithinBounds(t *testing.T) {
+	require.NoError(t, quick.Check(func(timeSinceUnixEpoch time.Duration, offset time.Duration) bool {
+		if offset < 0 {
+			offset = -offset
+		}
+		if timeSinceUnixEpoch < 0 {
+			timeSinceUnixEpoch = -timeSinceUnixEpoch
+		}
+
+		offset = offset % certValidity
+		// Bound this to 100 years
+		timeSinceUnixEpoch = time.Duration(timeSinceUnixEpoch % (time.Hour * 24 * 365 * 100))
+		// Start a bit further in the future to avoid edge cases around epoch
+		timeSinceUnixEpoch += time.Hour * 24 * 365
+		start := time.UnixMilli(timeSinceUnixEpoch.Milliseconds())
+
+		bucketStart := getCurrentBucketStartTime(start.Add(-clockSkewAllowance), offset)
+		return !bucketStart.After(start.Add(-clockSkewAllowance)) || bucketStart.Equal(start.Add(-clockSkewAllowance))
+	}, nil))
+}
diff --git a/p2p/transport/webtransport/crypto_test.go b/p2p/transport/webtransport/crypto_test.go
index d6d10620..143cd6de 100644
--- a/p2p/transport/webtransport/crypto_test.go
+++ b/p2p/transport/webtransport/crypto_test.go
@@ -1,6 +1,7 @@
 package libp2pwebtransport
 
 import (
+	"bytes"
 	"crypto"
 	"crypto/ecdsa"
 	"crypto/elliptic"
@@ -10,11 +11,13 @@ import (
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"fmt"
+	"io"
 	"math/big"
 	mrand "math/rand"
 	"testing"
 	"time"
 
+	ic "github.com/libp2p/go-libp2p/core/crypto"
 	"github.com/multiformats/go-multihash"
 	"github.com/stretchr/testify/require"
 )
@@ -130,3 +133,68 @@ func TestCertificateVerification(t *testing.T) {
 		})
 	}
 }
+
+func TestDeterministicCertHashes(t *testing.T) {
+	// Run this test 1000 times since we want to make sure the signatures are deterministic
+	runs := 1000
+	for i := 0; i < runs; i++ {
+		t.Run(fmt.Sprintf("Run=%d", i), func(t *testing.T) {
+			zeroSeed := [32]byte{}
+			priv, _, err := ic.GenerateEd25519Key(bytes.NewReader(zeroSeed[:]))
+			require.NoError(t, err)
+			cert, certPriv, err := generateCert(priv, time.Time{}, time.Time{}.Add(time.Hour*24*14))
+			require.NoError(t, err)
+
+			keyBytes, err := x509.MarshalECPrivateKey(certPriv)
+			require.NoError(t, err)
+
+			cert2, certPriv2, err := generateCert(priv, time.Time{}, time.Time{}.Add(time.Hour*24*14))
+			require.NoError(t, err)
+
+			require.Equal(t, cert2.Signature, cert.Signature)
+			require.Equal(t, cert2.Raw, cert.Raw)
+			keyBytes2, err := x509.MarshalECPrivateKey(certPriv2)
+			require.NoError(t, err)
+			require.Equal(t, keyBytes, keyBytes2)
+		})
+	}
+}
+
+// TestDeterministicSig tests that our hack around making ECDSA signatures
+// deterministic works. If this fails, this means we need to try another
+// strategy to make deterministic signatures or try something else entirely.
+// See deterministicReader for more context.
+func TestDeterministicSig(t *testing.T) {
+	// Run this test 1000 times since we want to make sure the signatures are deterministic
+	runs := 1000
+	for i := 0; i < runs; i++ {
+		t.Run(fmt.Sprintf("Run=%d", i), func(t *testing.T) {
+			zeroSeed := [32]byte{}
+			deterministicHKDFReader := newDeterministicReader(zeroSeed[:], nil, deterministicCertInfo)
+			b := [1024]byte{}
+			io.ReadFull(deterministicHKDFReader, b[:])
+			caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), deterministicHKDFReader)
+			require.NoError(t, err)
+
+			sig, err := caPrivateKey.Sign(deterministicHKDFReader, b[:], crypto.SHA256)
+			require.NoError(t, err)
+
+			deterministicHKDFReader = newDeterministicReader(zeroSeed[:], nil, deterministicCertInfo)
+			b2 := [1024]byte{}
+			io.ReadFull(deterministicHKDFReader, b2[:])
+			caPrivateKey2, err := ecdsa.GenerateKey(elliptic.P256(), deterministicHKDFReader)
+			require.NoError(t, err)
+
+			sig2, err := caPrivateKey2.Sign(deterministicHKDFReader, b2[:], crypto.SHA256)
+			require.NoError(t, err)
+
+			keyBytes, err := x509.MarshalECPrivateKey(caPrivateKey)
+			require.NoError(t, err)
+			keyBytes2, err := x509.MarshalECPrivateKey(caPrivateKey2)
+			require.NoError(t, err)
+
+			require.Equal(t, sig, sig2)
+			require.Equal(t, keyBytes, keyBytes2)
+		})
+	}
+}
diff --git a/p2p/transport/webtransport/transport_test.go b/p2p/transport/webtransport/transport_test.go
index de4cf226..70d4ec4b 100644
--- a/p2p/transport/webtransport/transport_test.go
+++ b/p2p/transport/webtransport/transport_test.go
@@ -20,14 +20,18 @@ import (
 	"strings"
 	"sync/atomic"
 	"testing"
+	"testing/quick"
 	"time"
 
+	"github.com/benbjohnson/clock"
 	ic "github.com/libp2p/go-libp2p/core/crypto"
 	"github.com/libp2p/go-libp2p/core/network"
 	mocknetwork "github.com/libp2p/go-libp2p/core/network/mocks"
 	"github.com/libp2p/go-libp2p/core/peer"
+	"github.com/libp2p/go-libp2p/core/test"
 	tpt "github.com/libp2p/go-libp2p/core/transport"
 	libp2pwebtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
+	"github.com/lucas-clemente/quic-go"
 
 	"github.com/golang/mock/gomock"
 	quicproxy "github.com/lucas-clemente/quic-go/integrationtests/tools/proxy"
@@ -38,6 +42,9 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+const clockSkewAllowance = time.Hour
+const certValidity = 14 * 24 * time.Hour
+
 func newIdentity(t *testing.T) (peer.ID, ic.PrivKey) {
 	key, _, err := ic.GenerateEd25519Key(rand.Reader)
 	require.NoError(t, err)
@@ -714,3 +721,195 @@ func TestFlowControlWindowIncrease(t *testing.T) {
 		t.Fatal("timeout")
 	}
 }
+
+var errTimeout = errors.New("timeout")
+
+func serverSendsBackValidCert(timeSinceUnixEpoch time.Duration, keySeed int64, randomClientSkew time.Duration) error {
+	if timeSinceUnixEpoch < 0 {
+		timeSinceUnixEpoch = -timeSinceUnixEpoch
+	}
+
+	// Bound this to 100 years
+	timeSinceUnixEpoch = time.Duration(timeSinceUnixEpoch % (time.Hour * 24 * 365 * 100))
+	// Start a bit further in the future to avoid edge cases around epoch
+	timeSinceUnixEpoch += time.Hour * 24 * 365
+	start := time.UnixMilli(timeSinceUnixEpoch.Milliseconds())
+
+	randomClientSkew = randomClientSkew % clockSkewAllowance
+
+	cl := clock.NewMock()
+	cl.Set(start)
+
+	priv, _, err := test.SeededTestKeyPair(ic.Ed25519, 256, keySeed)
+	if err != nil {
+		return err
+	}
+	tr, err := libp2pwebtransport.New(priv, nil, &network.NullResourceManager{}, libp2pwebtransport.WithClock(cl))
+	if err != nil {
+		return err
+	}
+	l, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	if err != nil {
+		return err
+	}
+	defer l.Close()
+
+	conn, err := quic.DialAddr(l.Addr().String(), &tls.Config{
+		NextProtos:         []string{"h3"},
+		InsecureSkipVerify: true,
+		VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+			for _, c := range rawCerts {
+				cert, err := x509.ParseCertificate(c)
+				if err != nil {
+					return err
+				}
+
+				for _, clientSkew := range []time.Duration{randomClientSkew, -clockSkewAllowance, clockSkewAllowance} {
+					clientTime := cl.Now().Add(clientSkew)
+					if clientTime.After(cert.NotAfter) || clientTime.Before(cert.NotBefore) {
+						return fmt.Errorf("Times are not valid: server_now=%v client_now=%v certstart=%v certend=%v", cl.Now().UTC(), clientTime.UTC(), cert.NotBefore.UTC(), cert.NotAfter.UTC())
+					}
+				}
+
+			}
+			return nil
+		},
+	}, &quic.Config{MaxIdleTimeout: time.Second})
+
+	if err != nil {
+		if _, ok := err.(*quic.IdleTimeoutError); ok {
+			return errTimeout
+		}
+		return err
+	}
+	defer conn.CloseWithError(0, "")
+
+	return nil
+}
+
+func TestServerSendsBackValidCert(t *testing.T) {
+	var maxTimeoutErrors = 10
+	require.NoError(t, quick.Check(func(timeSinceUnixEpoch time.Duration, keySeed int64, randomClientSkew time.Duration) bool {
+		err := serverSendsBackValidCert(timeSinceUnixEpoch, keySeed, randomClientSkew)
+		if err == errTimeout {
+			maxTimeoutErrors -= 1
+			if maxTimeoutErrors <= 0 {
+				fmt.Println("Too many timeout errors")
+				return false
+			}
+			// Sporadic timeout errors on macOS
+			return true
+		} else if err != nil {
+			fmt.Println("Err:", err)
+			return false
+		}
+
+		return true
+	}, nil))
+}
+
+func TestServerRotatesCertCorrectly(t *testing.T) {
+	require.NoError(t, quick.Check(func(timeSinceUnixEpoch time.Duration, keySeed int64) bool {
+		if timeSinceUnixEpoch < 0 {
+			timeSinceUnixEpoch = -timeSinceUnixEpoch
+		}
+
+		// Bound this to 100 years
+		timeSinceUnixEpoch = time.Duration(timeSinceUnixEpoch % (time.Hour * 24 * 365 * 100))
+		// Start a bit further in the future to avoid edge cases around epoch
+		timeSinceUnixEpoch += time.Hour * 24 * 365
+		start := time.UnixMilli(timeSinceUnixEpoch.Milliseconds())
+
+		cl := clock.NewMock()
+		cl.Set(start)
+
+		priv, _, err := test.SeededTestKeyPair(ic.Ed25519, 256, keySeed)
+		if err != nil {
+			return false
+		}
+		tr, err := libp2pwebtransport.New(priv, nil, &network.NullResourceManager{}, libp2pwebtransport.WithClock(cl))
+		if err != nil {
+			return false
+		}
+
+		l, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+		if err != nil {
+			return false
+		}
+		certhashes := extractCertHashes(l.Multiaddr())
+		l.Close()
+
+		// These two certificates together are valid for at most certValidity - (4*clockSkewAllowance)
+		cl.Add(certValidity - (4 * clockSkewAllowance) - time.Second)
+		tr, err = libp2pwebtransport.New(priv, nil, &network.NullResourceManager{}, libp2pwebtransport.WithClock(cl))
+		if err != nil {
+			return false
+		}
+
+		l, err = tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+		if err != nil {
+			return false
+		}
+		defer l.Close()
+
+		var found bool
+		ma.ForEach(l.Multiaddr(), func(c ma.Component) bool {
+			if c.Protocol().Code == ma.P_CERTHASH {
+				for _, prevCerthash := range certhashes {
+					if c.Value() == prevCerthash {
+						found = true
+						return false
+					}
+				}
+			}
+			return true
+		})
+
+		return found
+
+	}, nil))
+}
+
+func TestServerRotatesCertCorrectlyAfterSteps(t *testing.T) {
+	cl := clock.NewMock()
+	// Move one year ahead to avoid edge cases around epoch
+	cl.Add(time.Hour * 24 * 365)
+
+	priv, _, err := test.RandTestKeyPair(ic.Ed25519, 256)
+	require.NoError(t, err)
+	tr, err := libp2pwebtransport.New(priv, nil, &network.NullResourceManager{}, libp2pwebtransport.WithClock(cl))
+	require.NoError(t, err)
+
+	l, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	require.NoError(t, err)
+
+	certhashes := extractCertHashes(l.Multiaddr())
+	l.Close()
+
+	// Traverse various time boundaries and make sure we always keep a common certhash.
+	// e.g. certhash/A/certhash/B ... -> ... certhash/B/certhash/C ... -> ... certhash/C/certhash/D
+	for i := 0; i < 200; i++ {
+		cl.Add(24 * time.Hour)
+		tr, err := libp2pwebtransport.New(priv, nil, &network.NullResourceManager{}, libp2pwebtransport.WithClock(cl))
+		require.NoError(t, err)
+		l, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+		require.NoError(t, err)
+
+		var found bool
+		ma.ForEach(l.Multiaddr(), func(c ma.Component) bool {
+			if c.Protocol().Code == ma.P_CERTHASH {
+				for _, prevCerthash := range certhashes {
+					if prevCerthash == c.Value() {
+						found = true
+						return false
+					}
+				}
+			}
+			return true
+		})
+		certhashes = extractCertHashes(l.Multiaddr())
+		l.Close()
+
+		require.True(t, found, "Failed after hour: %v", i)
+	}
+}