Repository: libp2p/go-libp2p
Pull request #1736 (2022-09-02 16:47:24)

autorelay: Add a context.Context to WithPeerSource callback
The main point of this is to stop the search when the libp2p stops.
I've added that this *might* be used in the future to notify when AutoRelay is satisfied just to not close any door in the future but havn't implemented anything like that.


Patch:
diff --git a/config/config.go b/config/config.go
index 90fa9c16..0634ef4a 100644
--- a/config/config.go
+++ b/config/config.go
@@ -155,6 +155,9 @@ func (cfg *Config) makeSwarm() (*swarm.Swarm, error) {
 	if cfg.ResourceManager != nil {
 		opts = append(opts, swarm.WithResourceManager(cfg.ResourceManager))
 	}
+	if cfg.MultiaddrResolver != nil {
+		opts = append(opts, swarm.WithMultiaddrResolver(cfg.MultiaddrResolver))
+	}
 	// TODO: Make the swarm implementation configurable.
 	return swarm.NewSwarm(pid, cfg.Peerstore, opts...)
 }
@@ -229,7 +232,6 @@ func (cfg *Config) NewNode() (host.Host, error) {
 		EnablePing:          !cfg.DisablePing,
 		UserAgent:           cfg.UserAgent,
 		ProtocolVersion:     cfg.ProtocolVersion,
-		MultiaddrResolver:   cfg.MultiaddrResolver,
 		EnableHolePunching:  cfg.EnableHolePunching,
 		HolePunchingOptions: cfg.HolePunchingOptions,
 		EnableRelayService:  cfg.EnableRelayService,
diff --git a/core/transport/transport.go b/core/transport/transport.go
index 379e9d6d..ad2ee664 100644
--- a/core/transport/transport.go
+++ b/core/transport/transport.go
@@ -77,6 +77,12 @@ type Transport interface {
 	Proxy() bool
 }
 
+// Resolver can be optionally implemented by transports that want to resolve or transform the
+// multiaddr.
+type Resolver interface {
+	Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multiaddr, error)
+}
+
 // Listener is an interface closely resembling the net.Listener interface. The
 // only real difference is that Accept() returns Conn's of the type in this
 // package, and also exposes a Multiaddr method as opposed to a regular Addr
diff --git a/go.mod b/go.mod
index 3e8806ab..bd252dbf 100644
--- a/go.mod
+++ b/go.mod
@@ -29,17 +29,19 @@ require (
 	github.com/libp2p/go-netroute v0.2.0
 	github.com/libp2p/go-openssl v0.1.0
 	github.com/libp2p/go-reuseport v0.2.0
-	github.com/libp2p/go-yamux/v3 v3.1.2
+	github.com/libp2p/go-yamux/v4 v4.0.0
 	github.com/libp2p/zeroconf/v2 v2.2.0
-	github.com/lucas-clemente/quic-go v0.28.1
+	github.com/lucas-clemente/quic-go v0.29.0
 	github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd
+	github.com/marten-seemann/webtransport-go v0.1.0
 	github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b
 	github.com/minio/sha256-simd v1.0.0
 	github.com/mr-tron/base58 v1.2.0
 	github.com/multiformats/go-base32 v0.0.4
-	github.com/multiformats/go-multiaddr v0.6.0
+	github.com/multiformats/go-multiaddr v0.7.0
 	github.com/multiformats/go-multiaddr-dns v0.3.1
 	github.com/multiformats/go-multiaddr-fmt v0.1.0
+	github.com/multiformats/go-multibase v0.1.1
 	github.com/multiformats/go-multicodec v0.5.0
 	github.com/multiformats/go-multihash v0.2.1
 	github.com/multiformats/go-multistream v0.3.3
@@ -60,7 +62,6 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash v1.1.0 // indirect
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
-	github.com/cheekybits/genny v1.0.0 // indirect
 	github.com/containerd/cgroups v1.0.4 // indirect
 	github.com/coreos/go-systemd/v22 v22.3.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
@@ -82,8 +83,7 @@ require (
 	github.com/klauspost/cpuid/v2 v2.1.0 // indirect
 	github.com/koron/go-ssdp v0.0.3 // indirect
 	github.com/libp2p/go-cidranger v1.1.0 // indirect
-	github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
-	github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect
+	github.com/marten-seemann/qpack v0.2.1 // indirect
 	github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
 	github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
 	github.com/mattn/go-isatty v0.0.16 // indirect
@@ -92,7 +92,6 @@ require (
 	github.com/miekg/dns v1.1.50 // indirect
 	github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
 	github.com/multiformats/go-base36 v0.1.0 // indirect
-	github.com/multiformats/go-multibase v0.1.1 // indirect
 	github.com/nxadm/tail v1.4.8 // indirect
 	github.com/onsi/ginkgo v1.16.5 // indirect
 	github.com/opencontainers/runtime-spec v1.0.2 // indirect
@@ -107,8 +106,10 @@ require (
 	go.uber.org/atomic v1.10.0 // indirect
 	go.uber.org/multierr v1.8.0 // indirect
 	go.uber.org/zap v1.22.0 // indirect
+	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
 	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
 	golang.org/x/net v0.0.0-20220812174116-3211cb980234 // indirect
+	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/tools v0.1.12 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
diff --git a/go.sum b/go.sum
index 2049a41b..c419acc4 100644
--- a/go.sum
+++ b/go.sum
@@ -66,8 +66,6 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
-github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -314,27 +312,25 @@ github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6
 github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560=
 github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k=
 github.com/libp2p/go-sockaddr v0.0.2/go.mod h1:syPvOmNs24S3dFVGJA1/mrqdeijPxLV2Le3BRLKd68k=
-github.com/libp2p/go-yamux/v3 v3.1.2 h1:lNEy28MBk1HavUAlzKgShp+F6mn/ea1nDYWftZhFW9Q=
-github.com/libp2p/go-yamux/v3 v3.1.2/go.mod h1:jeLEQgLXqE2YqX1ilAClIfCMDY+0uXQUKmmb/qp0gT4=
+github.com/libp2p/go-yamux/v4 v4.0.0 h1:+Y80dV2Yx/kv7Y7JKu0LECyVdMXm1VUoko+VQ9rBfZQ=
+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.28.1 h1:Uo0lvVxWg5la9gflIF9lwa39ONq85Xq2D91YNEIslzU=
-github.com/lucas-clemente/quic-go v0.28.1/go.mod h1:oGz5DKK41cJt5+773+BSO9BXDsREY4HLf7+0odGAPO0=
+github.com/lucas-clemente/quic-go v0.29.0 h1:Vw0mGTfmWqGzh4jx/kMymsIkFK6rErFVmg+t9RLrnZE=
+github.com/lucas-clemente/quic-go v0.29.0/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
 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=
+github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
 github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
-github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ=
-github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
-github.com/marten-seemann/qtls-go1-17 v0.1.2 h1:JADBlm0LYiVbuSySCHeY863dNkcpMmDR7s0bLKJeYlQ=
-github.com/marten-seemann/qtls-go1-17 v0.1.2/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s=
 github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
 github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
-github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
 github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
 github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
 github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
 github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
+github.com/marten-seemann/webtransport-go v0.1.0 h1:tKknMOI+9fmMTPRJv3Fz7XpDq67Tg8Va8B1QEE+qaZw=
+github.com/marten-seemann/webtransport-go v0.1.0/go.mod h1:CACJOFSohYRmnqHWHWZ2rpTr9MfA0pNqOSkpqb6J/+w=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -373,8 +369,8 @@ github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ8
 github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM=
 github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
 github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4=
-github.com/multiformats/go-multiaddr v0.6.0 h1:qMnoOPj2s8xxPU5kZ57Cqdr0hHhARz7mFsPMIiYNqzg=
-github.com/multiformats/go-multiaddr v0.6.0/go.mod h1:F4IpaKZuPP360tOMn2Tpyu0At8w23aRyVqeK0DbFeGM=
+github.com/multiformats/go-multiaddr v0.7.0 h1:gskHcdaCyPtp9XskVwtvEeQOG465sCohbQIirSyqxrc=
+github.com/multiformats/go-multiaddr v0.7.0/go.mod h1:Fs50eBDWvZu+l3/9S6xAE7ZYj6yhxlvaVZjakWN7xRs=
 github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A=
 github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk=
 github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
@@ -402,15 +398,12 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
-github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
-github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
-github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
 github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0=
 github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
 github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
@@ -556,7 +549,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
@@ -572,6 +564,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
+golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -637,12 +631,10 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
-golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E=
 golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -729,7 +721,6 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 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-20220520151302-bc2c85ada10a/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=
diff --git a/p2p/host/autorelay/options.go b/p2p/host/autorelay/options.go
index e62f129d..a6e9f595 100644
--- a/p2p/host/autorelay/options.go
+++ b/p2p/host/autorelay/options.go
@@ -1,6 +1,7 @@
 package autorelay
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"time"
@@ -12,7 +13,7 @@ import (
 
 type config struct {
 	clock      clock.Clock
-	peerSource func(num int) <-chan peer.AddrInfo
+	peerSource func(ctx context.Context, num int) <-chan peer.AddrInfo
 	// minimum interval used to call the peerSource callback
 	minInterval  time.Duration
 	staticRelays []peer.AddrInfo
@@ -102,7 +103,9 @@ func WithDefaultStaticRelays() Option {
 // Implementations should send new peers, but may send peers they sent before. AutoRelay implements
 // a per-peer backoff (see WithBackoff).
 // minInterval is the minimum interval this callback is called with, even if AutoRelay needs new candidates.
-func WithPeerSource(f func(numPeers int) <-chan peer.AddrInfo, minInterval time.Duration) Option {
+// The context.Context passed MAY be canceled when AutoRelay feels satisfied, it will be canceled when the node is shutting down.
+// If the channel is canceled you MUST close the output channel at some point.
+func WithPeerSource(f func(ctx context.Context, numPeers int) <-chan peer.AddrInfo, minInterval time.Duration) Option {
 	return func(c *config) error {
 		if len(c.staticRelays) > 0 {
 			return errStaticRelaysPeerSource
diff --git a/p2p/host/autorelay/relay_finder.go b/p2p/host/autorelay/relay_finder.go
index a62947d3..4760511b 100644
--- a/p2p/host/autorelay/relay_finder.go
+++ b/p2p/host/autorelay/relay_finder.go
@@ -59,7 +59,7 @@ type relayFinder struct {
 	ctxCancel   context.CancelFunc
 	ctxCancelMx sync.Mutex
 
-	peerSource func(int) <-chan peer.AddrInfo
+	peerSource func(context.Context, int) <-chan peer.AddrInfo
 
 	candidateFound             chan struct{} // receives every time we find a new relay candidate
 	candidateMx                sync.Mutex
@@ -82,7 +82,7 @@ type relayFinder struct {
 	cachedAddrsExpiry time.Time
 }
 
-func newRelayFinder(host *basic.BasicHost, peerSource func(int) <-chan peer.AddrInfo, conf *config) *relayFinder {
+func newRelayFinder(host *basic.BasicHost, peerSource func(context.Context, int) <-chan peer.AddrInfo, conf *config) *relayFinder {
 	return &relayFinder{
 		bootTime:                   conf.clock.Now(),
 		host:                       host,
@@ -206,7 +206,7 @@ func (rf *relayFinder) background(ctx context.Context) {
 // It garbage collects old entries, so that nodes doesn't overflow.
 // This makes sure that as soon as we need to find relay candidates, we have them available.
 func (rf *relayFinder) findNodes(ctx context.Context) {
-	peerChan := rf.peerSource(rf.conf.maxCandidates)
+	peerChan := rf.peerSource(ctx, rf.conf.maxCandidates)
 	var wg sync.WaitGroup
 	lastCallToPeerSource := rf.conf.clock.Now()
 
@@ -235,7 +235,7 @@ func (rf *relayFinder) findNodes(ctx context.Context) {
 				continue
 			}
 			lastCallToPeerSource = now
-			peerChan = rf.peerSource(rf.conf.maxCandidates)
+			peerChan = rf.peerSource(ctx, rf.conf.maxCandidates)
 		case pi, ok := <-peerChan:
 			if !ok {
 				wg.Wait()
diff --git a/p2p/host/basic/basic_host.go b/p2p/host/basic/basic_host.go
index 99978763..19fff7d2 100644
--- a/p2p/host/basic/basic_host.go
+++ b/p2p/host/basic/basic_host.go
@@ -38,10 +38,6 @@ import (
 	msmux "github.com/multiformats/go-multistream"
 )
 
-// The maximum number of address resolution steps we'll perform for a single
-// peer (for all addresses).
-const maxAddressResolution = 32
-
 // addrChangeTickrInterval is the interval between two address change ticks.
 var addrChangeTickrInterval = 5 * time.Second
 
@@ -713,77 +709,9 @@ func (h *BasicHost) Connect(ctx context.Context, pi peer.AddrInfo) error {
 		}
 	}
 
-	resolved, err := h.resolveAddrs(ctx, h.Peerstore().PeerInfo(pi.ID))
-	if err != nil {
-		return err
-	}
-	h.Peerstore().AddAddrs(pi.ID, resolved, peerstore.TempAddrTTL)
-
 	return h.dialPeer(ctx, pi.ID)
 }
 
-func (h *BasicHost) resolveAddrs(ctx context.Context, pi peer.AddrInfo) ([]ma.Multiaddr, error) {
-	proto := ma.ProtocolWithCode(ma.P_P2P).Name
-	p2paddr, err := ma.NewMultiaddr("/" + proto + "/" + pi.ID.Pretty())
-	if err != nil {
-		return nil, err
-	}
-
-	resolveSteps := 0
-
-	// Recursively resolve all addrs.
-	//
-	// While the toResolve list is non-empty:
-	// * Pop an address off.
-	// * If the address is fully resolved, add it to the resolved list.
-	// * Otherwise, resolve it and add the results to the "to resolve" list.
-	toResolve := append(([]ma.Multiaddr)(nil), pi.Addrs...)
-	resolved := make([]ma.Multiaddr, 0, len(pi.Addrs))
-	for len(toResolve) > 0 {
-		// pop the last addr off.
-		addr := toResolve[len(toResolve)-1]
-		toResolve = toResolve[:len(toResolve)-1]
-
-		// if it's resolved, add it to the resolved list.
-		if !madns.Matches(addr) {
-			resolved = append(resolved, addr)
-			continue
-		}
-
-		resolveSteps++
-
-		// We've resolved too many addresses. We can keep all the fully
-		// resolved addresses but we'll need to skip the rest.
-		if resolveSteps >= maxAddressResolution {
-			log.Warnf(
-				"peer %s asked us to resolve too many addresses: %s/%s",
-				pi.ID,
-				resolveSteps,
-				maxAddressResolution,
-			)
-			continue
-		}
-
-		// otherwise, resolve it
-		reqaddr := addr.Encapsulate(p2paddr)
-		resaddrs, err := h.maResolver.Resolve(ctx, reqaddr)
-		if err != nil {
-			log.Infof("error resolving %s: %s", reqaddr, err)
-		}
-
-		// add the results to the toResolve list.
-		for _, res := range resaddrs {
-			pi, err := peer.AddrInfoFromP2pAddr(res)
-			if err != nil {
-				log.Infof("error parsing %s: %s", res, err)
-			}
-			toResolve = append(toResolve, pi.Addrs...)
-		}
-	}
-
-	return resolved, nil
-}
-
 // dialPeer opens a connection to peer, and makes sure to identify
 // the connection once it has been opened.
 func (h *BasicHost) dialPeer(ctx context.Context, p peer.ID) error {
diff --git a/p2p/host/resource-manager/README.md b/p2p/host/resource-manager/README.md
index 2d2ab335..96edd1d0 100644
--- a/p2p/host/resource-manager/README.md
+++ b/p2p/host/resource-manager/README.md
@@ -449,7 +449,7 @@ Look at `WithAllowlistedMultiaddrs` and its example in the GoDoc to learn more.
 ## ConnManager vs Resource Manager
 
 go-libp2p already includes a [connection
-manager](https://pkg.go.dev/github.com/libp2p/go-libp2p-core/connmgr#ConnManager),
+manager](https://pkg.go.dev/github.com/libp2p/go-libp2p/core/connmgr#ConnManager),
 so what's the difference between the `ConnManager` and the `ResourceManager`?
 
 ConnManager:
diff --git a/p2p/host/resource-manager/limit_defaults.go b/p2p/host/resource-manager/limit_defaults.go
index 3a973d99..dde3e7b7 100644
--- a/p2p/host/resource-manager/limit_defaults.go
+++ b/p2p/host/resource-manager/limit_defaults.go
@@ -514,7 +514,7 @@ var DefaultLimits = ScalingLimitConfig{
 		ConnsOutbound: 1,
 		Conns:         1,
 		FD:            1,
-		Memory:        1 << 20,
+		Memory:        32 << 20,
 	},
 
 	StreamBaseLimit: BaseLimit{
diff --git a/p2p/muxer/yamux/conn.go b/p2p/muxer/yamux/conn.go
index 30f9f351..40c4af40 100644
--- a/p2p/muxer/yamux/conn.go
+++ b/p2p/muxer/yamux/conn.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/libp2p/go-libp2p/core/network"
 
-	"github.com/libp2p/go-yamux/v3"
+	"github.com/libp2p/go-yamux/v4"
 )
 
 // conn implements mux.MuxedConn over yamux.Session.
diff --git a/p2p/muxer/yamux/stream.go b/p2p/muxer/yamux/stream.go
index e6a3a04b..b50bc0bb 100644
--- a/p2p/muxer/yamux/stream.go
+++ b/p2p/muxer/yamux/stream.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/libp2p/go-libp2p/core/network"
 
-	"github.com/libp2p/go-yamux/v3"
+	"github.com/libp2p/go-yamux/v4"
 )
 
 // stream implements mux.MuxedStream over yamux.Stream.
diff --git a/p2p/muxer/yamux/transport.go b/p2p/muxer/yamux/transport.go
index bba53d7a..df4ff1be 100644
--- a/p2p/muxer/yamux/transport.go
+++ b/p2p/muxer/yamux/transport.go
@@ -7,7 +7,7 @@ import (
 
 	"github.com/libp2p/go-libp2p/core/network"
 
-	"github.com/libp2p/go-yamux/v3"
+	"github.com/libp2p/go-yamux/v4"
 )
 
 var DefaultTransport *Transport
@@ -38,12 +38,17 @@ type Transport yamux.Config
 var _ network.Multiplexer = &Transport{}
 
 func (t *Transport) NewConn(nc net.Conn, isServer bool, scope network.PeerScope) (network.MuxedConn, error) {
+	var newSpan func() (yamux.MemoryManager, error)
+	if scope != nil {
+		newSpan = func() (yamux.MemoryManager, error) { return scope.BeginSpan() }
+	}
+
 	var s *yamux.Session
 	var err error
 	if isServer {
-		s, err = yamux.Server(nc, t.Config(), scope)
+		s, err = yamux.Server(nc, t.Config(), newSpan)
 	} else {
-		s, err = yamux.Client(nc, t.Config(), scope)
+		s, err = yamux.Client(nc, t.Config(), newSpan)
 	}
 	if err != nil {
 		return nil, err
diff --git a/p2p/net/swarm/swarm.go b/p2p/net/swarm/swarm.go
index 80d67eb9..ece0986d 100644
--- a/p2p/net/swarm/swarm.go
+++ b/p2p/net/swarm/swarm.go
@@ -19,6 +19,7 @@ import (
 
 	logging "github.com/ipfs/go-log/v2"
 	ma "github.com/multiformats/go-multiaddr"
+	madns "github.com/multiformats/go-multiaddr-dns"
 )
 
 const (
@@ -54,6 +55,14 @@ func WithConnectionGater(gater connmgr.ConnectionGater) Option {
 	}
 }
 
+// WithMultiaddrResolver sets a custom multiaddress resolver
+func WithMultiaddrResolver(maResolver *madns.Resolver) Option {
+	return func(s *Swarm) error {
+		s.maResolver = maResolver
+		return nil
+	}
+}
+
 // WithMetrics sets a metrics reporter
 func WithMetrics(reporter metrics.Reporter) Option {
 	return func(s *Swarm) error {
@@ -127,6 +136,8 @@ type Swarm struct {
 		m map[int]transport.Transport
 	}
 
+	maResolver *madns.Resolver
+
 	// stream handlers
 	streamh atomic.Value
 
@@ -153,6 +164,7 @@ func NewSwarm(local peer.ID, peers peerstore.Peerstore, opts ...Option) (*Swarm,
 		ctxCancel:        cancel,
 		dialTimeout:      defaultDialTimeout,
 		dialTimeoutLocal: defaultDialTimeoutLocal,
+		maResolver:       madns.DefaultResolver,
 	}
 
 	s.conns.m = make(map[peer.ID][]*Conn)
diff --git a/p2p/net/swarm/swarm_dial.go b/p2p/net/swarm/swarm_dial.go
index 0e232a18..bba2c462 100644
--- a/p2p/net/swarm/swarm_dial.go
+++ b/p2p/net/swarm/swarm_dial.go
@@ -10,12 +10,18 @@ import (
 	"github.com/libp2p/go-libp2p/core/canonicallog"
 	"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/transport"
 
 	ma "github.com/multiformats/go-multiaddr"
+	madns "github.com/multiformats/go-multiaddr-dns"
 	manet "github.com/multiformats/go-multiaddr/net"
 )
 
+// The maximum number of address resolution steps we'll perform for a single
+// peer (for all addresses).
+const maxAddressResolution = 32
+
 // Diagram of dial sync:
 //
 //   many callers of Dial()   synched w.  dials many addrs       results to callers
@@ -292,7 +298,32 @@ func (s *Swarm) addrsForDial(ctx context.Context, p peer.ID) ([]ma.Multiaddr, er
 		return nil, ErrNoAddresses
 	}
 
-	goodAddrs := s.filterKnownUndialables(p, peerAddrs)
+	peerAddrsAfterTransportResolved := make([]ma.Multiaddr, 0, len(peerAddrs))
+	for _, a := range peerAddrs {
+		tpt := s.TransportForDialing(a)
+		resolver, ok := tpt.(transport.Resolver)
+		if ok {
+			resolvedAddrs, err := resolver.Resolve(ctx, a)
+			if err != nil {
+				log.Warnf("Failed to resolve multiaddr %s by transport %v: %v", a, tpt, err)
+				continue
+			}
+			peerAddrsAfterTransportResolved = append(peerAddrsAfterTransportResolved, resolvedAddrs...)
+		} else {
+			peerAddrsAfterTransportResolved = append(peerAddrsAfterTransportResolved, a)
+		}
+	}
+
+	// Resolve dns or dnsaddrs
+	resolved, err := s.resolveAddrs(ctx, peer.AddrInfo{
+		ID:    p,
+		Addrs: peerAddrsAfterTransportResolved,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	goodAddrs := s.filterKnownUndialables(p, resolved)
 	if forceDirect, _ := network.GetForceDirectDial(ctx); forceDirect {
 		goodAddrs = ma.FilterAddrs(goodAddrs, s.nonProxyAddr)
 	}
@@ -301,7 +332,71 @@ func (s *Swarm) addrsForDial(ctx context.Context, p peer.ID) ([]ma.Multiaddr, er
 		return nil, ErrNoGoodAddresses
 	}
 
-	return goodAddrs, nil
+	s.peers.AddAddrs(p, goodAddrs, peerstore.TempAddrTTL)
+
+	return resolved, nil
+}
+
+func (s *Swarm) resolveAddrs(ctx context.Context, pi peer.AddrInfo) ([]ma.Multiaddr, error) {
+	proto := ma.ProtocolWithCode(ma.P_P2P).Name
+	p2paddr, err := ma.NewMultiaddr("/" + proto + "/" + pi.ID.Pretty())
+	if err != nil {
+		return nil, err
+	}
+
+	resolveSteps := 0
+
+	// Recursively resolve all addrs.
+	//
+	// While the toResolve list is non-empty:
+	// * Pop an address off.
+	// * If the address is fully resolved, add it to the resolved list.
+	// * Otherwise, resolve it and add the results to the "to resolve" list.
+	toResolve := append(([]ma.Multiaddr)(nil), pi.Addrs...)
+	resolved := make([]ma.Multiaddr, 0, len(pi.Addrs))
+	for len(toResolve) > 0 {
+		// pop the last addr off.
+		addr := toResolve[len(toResolve)-1]
+		toResolve = toResolve[:len(toResolve)-1]
+
+		// if it's resolved, add it to the resolved list.
+		if !madns.Matches(addr) {
+			resolved = append(resolved, addr)
+			continue
+		}
+
+		resolveSteps++
+
+		// We've resolved too many addresses. We can keep all the fully
+		// resolved addresses but we'll need to skip the rest.
+		if resolveSteps >= maxAddressResolution {
+			log.Warnf(
+				"peer %s asked us to resolve too many addresses: %s/%s",
+				pi.ID,
+				resolveSteps,
+				maxAddressResolution,
+			)
+			continue
+		}
+
+		// otherwise, resolve it
+		reqaddr := addr.Encapsulate(p2paddr)
+		resaddrs, err := s.maResolver.Resolve(ctx, reqaddr)
+		if err != nil {
+			log.Infof("error resolving %s: %s", reqaddr, err)
+		}
+
+		// add the results to the toResolve list.
+		for _, res := range resaddrs {
+			pi, err := peer.AddrInfoFromP2pAddr(res)
+			if err != nil {
+				log.Infof("error parsing %s: %s", res, err)
+			}
+			toResolve = append(toResolve, pi.Addrs...)
+		}
+	}
+
+	return resolved, nil
 }
 
 func (s *Swarm) dialNextAddr(ctx context.Context, p peer.ID, addr ma.Multiaddr, resch chan dialResult) error {
diff --git a/p2p/security/noise/handshake.go b/p2p/security/noise/handshake.go
index b71d7ef8..c5aa968c 100644
--- a/p2p/security/noise/handshake.go
+++ b/p2p/security/noise/handshake.go
@@ -10,17 +10,16 @@ import (
 	"runtime/debug"
 	"time"
 
-	"github.com/minio/sha256-simd"
 	"golang.org/x/crypto/chacha20poly1305"
 
-	"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
-
 	"github.com/libp2p/go-libp2p/core/crypto"
 	"github.com/libp2p/go-libp2p/core/peer"
+	"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
 
 	"github.com/flynn/noise"
 	"github.com/gogo/protobuf/proto"
 	pool "github.com/libp2p/go-buffer-pool"
+	"github.com/minio/sha256-simd"
 )
 
 // payloadSigPrefix is prepended to our Noise static key before signing with
@@ -89,10 +88,12 @@ func (s *secureSession) runHandshake(ctx context.Context) (err error) {
 
 	if s.initiator {
 		// stage 0 //
-		// do not send the payload just yet, as it would be plaintext; not secret.
 		// Handshake Msg Len = len(DH ephemeral key)
-		err = s.sendHandshakeMessage(hs, nil, hbuf)
-		if err != nil {
+		var ed []byte
+		if s.earlyDataHandler != nil {
+			ed = s.earlyDataHandler.Send(ctx, s.insecureConn, s.remoteID)
+		}
+		if err := s.sendHandshakeMessage(hs, ed, hbuf); err != nil {
 			return fmt.Errorf("error sending handshake message: %w", err)
 		}
 
@@ -101,29 +102,32 @@ func (s *secureSession) runHandshake(ctx context.Context) (err error) {
 		if err != nil {
 			return fmt.Errorf("error reading handshake message: %w", err)
 		}
-		err = s.handleRemoteHandshakePayload(plaintext, hs.PeerStatic())
-		if err != nil {
+		if err := s.handleRemoteHandshakePayload(plaintext, hs.PeerStatic()); err != nil {
 			return err
 		}
 
 		// stage 2 //
 		// Handshake Msg Len = len(DHT static key) +  MAC(static key is encrypted) + len(Payload) + MAC(payload is encrypted)
-		err = s.sendHandshakeMessage(hs, payload, hbuf)
-		if err != nil {
+		if err := s.sendHandshakeMessage(hs, payload, hbuf); err != nil {
 			return fmt.Errorf("error sending handshake message: %w", err)
 		}
+		return nil
 	} else {
 		// stage 0 //
-		// We don't expect any payload on the first message.
-		if _, err := s.readHandshakeMessage(hs); err != nil {
+		initialPayload, err := s.readHandshakeMessage(hs)
+		if err != nil {
 			return fmt.Errorf("error reading handshake message: %w", err)
 		}
+		if s.earlyDataHandler != nil {
+			if err := s.earlyDataHandler.Received(ctx, s.insecureConn, initialPayload); err != nil {
+				return err
+			}
+		}
 
 		// stage 1 //
 		// Handshake Msg Len = len(DH ephemeral key) + len(DHT static key) +  MAC(static key is encrypted) + len(Payload) +
 		// MAC(payload is encrypted)
-		err = s.sendHandshakeMessage(hs, payload, hbuf)
-		if err != nil {
+		if err := s.sendHandshakeMessage(hs, payload, hbuf); err != nil {
 			return fmt.Errorf("error sending handshake message: %w", err)
 		}
 
@@ -132,13 +136,8 @@ func (s *secureSession) runHandshake(ctx context.Context) (err error) {
 		if err != nil {
 			return fmt.Errorf("error reading handshake message: %w", err)
 		}
-		err = s.handleRemoteHandshakePayload(plaintext, hs.PeerStatic())
-		if err != nil {
-			return err
-		}
+		return s.handleRemoteHandshakePayload(plaintext, hs.PeerStatic())
 	}
-
-	return nil
 }
 
 // setCipherStates sets the initial cipher states that will be used to protect
diff --git a/p2p/security/noise/session.go b/p2p/security/noise/session.go
index a563e58b..5e3d0956 100644
--- a/p2p/security/noise/session.go
+++ b/p2p/security/noise/session.go
@@ -36,20 +36,22 @@ type secureSession struct {
 	dec *noise.CipherState
 
 	// noise prologue
-	prologue []byte
+	prologue         []byte
+	earlyDataHandler EarlyDataHandler
 }
 
 // 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, initiator bool) (*secureSession, error) {
+func newSecureSession(tpt *Transport, ctx context.Context, insecure net.Conn, remote peer.ID, prologue []byte, edh EarlyDataHandler, initiator bool) (*secureSession, error) {
 	s := &secureSession{
-		insecureConn:   insecure,
-		insecureReader: bufio.NewReader(insecure),
-		initiator:      initiator,
-		localID:        tpt.localID,
-		localKey:       tpt.privateKey,
-		remoteID:       remote,
-		prologue:       prologue,
+		insecureConn:     insecure,
+		insecureReader:   bufio.NewReader(insecure),
+		initiator:        initiator,
+		localID:          tpt.localID,
+		localKey:         tpt.privateKey,
+		remoteID:         remote,
+		prologue:         prologue,
+		earlyDataHandler: edh,
 	}
 
 	// 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 b4414136..973f6fac 100644
--- a/p2p/security/noise/session_transport.go
+++ b/p2p/security/noise/session_transport.go
@@ -12,6 +12,32 @@ import (
 
 type SessionOption = func(*SessionTransport) error
 
+// Prologue sets a prologue for the Noise session.
+// The handshake will only complete successfully if both parties set the same prologue.
+// See https://noiseprotocol.org/noise.html#prologue for details.
+func Prologue(prologue []byte) SessionOption {
+	return func(s *SessionTransport) error {
+		s.prologue = prologue
+		return nil
+	}
+}
+
+// EarlyDataHandler allows attaching an (unencrypted) application payload to the first handshake message.
+// While unencrypted, the integrity of this early data is retroactively authenticated on completion of the handshake.
+type EarlyDataHandler interface {
+	// Send is called for the client before sending the first handshake message.
+	Send(context.Context, net.Conn, peer.ID) []byte
+	// Received is called for the server when the first handshake message from the client is received.
+	Received(context.Context, net.Conn, []byte) error
+}
+
+func EarlyData(h EarlyDataHandler) SessionOption {
+	return func(s *SessionTransport) error {
+		s.earlyDataHandler = h
+		return nil
+	}
+}
+
 var _ sec.SecureTransport = &SessionTransport{}
 
 // SessionTransport can be used
@@ -19,13 +45,14 @@ var _ sec.SecureTransport = &SessionTransport{}
 type SessionTransport struct {
 	t *Transport
 	// options
-	prologue []byte
+	prologue         []byte
+	earlyDataHandler EarlyDataHandler
 }
 
 // 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, false)
+	c, err := newSecureSession(i.t, ctx, insecure, p, i.prologue, i.earlyDataHandler, false)
 	if err != nil {
 		addr, maErr := manet.FromNetAddr(insecure.RemoteAddr())
 		if maErr == nil {
@@ -37,22 +64,5 @@ 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, true)
-}
-
-func (t *Transport) WithSessionOptions(opts ...SessionOption) (sec.SecureTransport, error) {
-	st := &SessionTransport{t: t}
-	for _, opt := range opts {
-		if err := opt(st); err != nil {
-			return nil, err
-		}
-	}
-	return st, nil
-}
-
-func Prologue(prologue []byte) SessionOption {
-	return func(s *SessionTransport) error {
-		s.prologue = prologue
-		return nil
-	}
+	return newSecureSession(i.t, ctx, insecure, p, i.prologue, i.earlyDataHandler, true)
 }
diff --git a/p2p/security/noise/transport.go b/p2p/security/noise/transport.go
index f935b82a..bd66d0fd 100644
--- a/p2p/security/noise/transport.go
+++ b/p2p/security/noise/transport.go
@@ -41,7 +41,7 @@ func New(privkey crypto.PrivKey) (*Transport, error) {
 // SecureInbound runs the Noise handshake as the responder.
 // 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) {
-	c, err := newSecureSession(t, ctx, insecure, p, nil, false)
+	c, err := newSecureSession(t, ctx, insecure, p, nil, nil, false)
 	if err != nil {
 		addr, maErr := manet.FromNetAddr(insecure.RemoteAddr())
 		if maErr == nil {
@@ -53,5 +53,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) {
-	return newSecureSession(t, ctx, insecure, p, nil, true)
+	return newSecureSession(t, ctx, insecure, p, nil, nil, true)
+}
+
+func (t *Transport) WithSessionOptions(opts ...SessionOption) (sec.SecureTransport, error) {
+	st := &SessionTransport{t: t}
+	for _, opt := range opts {
+		if err := opt(st); err != nil {
+			return nil, err
+		}
+	}
+	return st, nil
 }
diff --git a/p2p/transport/quic/tracer_metrics.go b/p2p/transport/quic/tracer_metrics.go
index cbf2701f..624139fc 100644
--- a/p2p/transport/quic/tracer_metrics.go
+++ b/p2p/transport/quic/tracer_metrics.go
@@ -167,7 +167,9 @@ func init() {
 	prometheus.MustRegister(collector)
 }
 
-type metricsTracer struct{}
+type metricsTracer struct {
+	logging.NullTracer
+}
 
 var _ logging.Tracer = &metricsTracer{}
 
@@ -179,10 +181,9 @@ func (m *metricsTracer) SentPacket(_ net.Addr, _ *logging.Header, size logging.B
 	bytesTransferred.WithLabelValues("sent").Add(float64(size))
 }
 
-func (m *metricsTracer) DroppedPacket(addr net.Addr, packetType logging.PacketType, count logging.ByteCount, reason logging.PacketDropReason) {
-}
-
 type metricsConnTracer struct {
+	logging.NullConnectionTracer
+
 	perspective       logging.Perspective
 	startTime         time.Time
 	connID            logging.ConnectionID
@@ -224,9 +225,6 @@ func (m *metricsConnTracer) StartedConnection(net.Addr, net.Addr, logging.Connec
 	collector.AddConn(m.connID.String(), m)
 }
 
-func (m *metricsConnTracer) NegotiatedVersion(chosen quic.VersionNumber, clientVersions []quic.VersionNumber, serverVersions []quic.VersionNumber) {
-}
-
 func (m *metricsConnTracer) ClosedConnection(e error) {
 	var (
 		applicationErr      *quic.ApplicationError
@@ -264,9 +262,6 @@ func (m *metricsConnTracer) ClosedConnection(e error) {
 	}
 	connErrors.WithLabelValues(side, desc).Inc()
 }
-func (m *metricsConnTracer) SentTransportParameters(parameters *logging.TransportParameters)     {}
-func (m *metricsConnTracer) ReceivedTransportParameters(parameters *logging.TransportParameters) {}
-func (m *metricsConnTracer) RestoredTransportParameters(parameters *logging.TransportParameters) {}
 func (m *metricsConnTracer) SentPacket(hdr *logging.ExtendedHeader, size logging.ByteCount, _ *logging.AckFrame, _ []logging.Frame) {
 	bytesTransferred.WithLabelValues("sent").Add(float64(size))
 	sentPackets.WithLabelValues(m.getEncLevel(logging.PacketTypeFromHeader(&hdr.Header))).Inc()
@@ -329,8 +324,6 @@ func (m *metricsConnTracer) UpdatedMetrics(rttStats *logging.RTTStats, cwnd, byt
 	m.mutex.Unlock()
 }
 
-func (m *metricsConnTracer) AcknowledgedPacket(logging.EncryptionLevel, logging.PacketNumber) {}
-
 func (m *metricsConnTracer) LostPacket(level logging.EncryptionLevel, _ logging.PacketNumber, r logging.PacketLossReason) {
 	var reason string
 	switch r {
@@ -344,23 +337,11 @@ func (m *metricsConnTracer) LostPacket(level logging.EncryptionLevel, _ logging.
 	lostPackets.WithLabelValues(level.String(), reason).Inc()
 }
 
-func (m *metricsConnTracer) UpdatedCongestionState(state logging.CongestionState) {}
-func (m *metricsConnTracer) UpdatedPTOCount(value uint32)                         {}
-func (m *metricsConnTracer) UpdatedKeyFromTLS(level logging.EncryptionLevel, perspective logging.Perspective) {
-}
-func (m *metricsConnTracer) UpdatedKey(generation logging.KeyPhase, remote bool) {}
 func (m *metricsConnTracer) DroppedEncryptionLevel(level logging.EncryptionLevel) {
 	if level == logging.EncryptionHandshake {
 		m.handleHandshakeComplete()
 	}
 }
-func (m *metricsConnTracer) DroppedKey(generation logging.KeyPhase) {}
-func (m *metricsConnTracer) SetLossTimer(timerType logging.TimerType, level logging.EncryptionLevel, time time.Time) {
-}
-
-func (m *metricsConnTracer) LossTimerExpired(timerType logging.TimerType, level logging.EncryptionLevel) {
-}
-func (m *metricsConnTracer) LossTimerCanceled() {}
 
 func (m *metricsConnTracer) Close() {
 	if m.handshakeComplete {
@@ -371,8 +352,6 @@ func (m *metricsConnTracer) Close() {
 	collector.RemoveConn(m.connID.String())
 }
 
-func (m *metricsConnTracer) Debug(name, msg string) {}
-
 func (m *metricsConnTracer) handleHandshakeComplete() {
 	m.handshakeComplete = true
 	newConns.WithLabelValues(m.getDirection(), "true").Inc()
diff --git a/p2p/transport/quic/transport.go b/p2p/transport/quic/transport.go
index 1ed3b05f..0b908db4 100644
--- a/p2p/transport/quic/transport.go
+++ b/p2p/transport/quic/transport.go
@@ -43,9 +43,9 @@ var quicConfig = &quic.Config{
 	MaxIncomingUniStreams:      -1,             // disable unidirectional streams
 	MaxStreamReceiveWindow:     10 * (1 << 20), // 10 MB
 	MaxConnectionReceiveWindow: 15 * (1 << 20), // 15 MB
-	AcceptToken: func(clientAddr net.Addr, _ *quic.Token) bool {
-		// TODO(#6): require source address validation when under load
-		return true
+	RequireAddressValidation: func(net.Addr) bool {
+		// TODO(#1535): require source address validation when under load
+		return false
 	},
 	KeepAlivePeriod: 15 * time.Second,
 	Versions:        []quic.VersionNumber{quic.VersionDraft29, quic.Version1},
diff --git a/p2p/transport/tcp/tcp.go b/p2p/transport/tcp/tcp.go
index 9e6f5386..61bb38e2 100644
--- a/p2p/transport/tcp/tcp.go
+++ b/p2p/transport/tcp/tcp.go
@@ -106,14 +106,21 @@ func WithConnectionTimeout(d time.Duration) Option {
 	}
 }
 
+func WithMetrics() Option {
+	return func(tr *TcpTransport) error {
+		tr.enableMetrics = true
+		return nil
+	}
+}
+
 // TcpTransport is the TCP transport.
 type TcpTransport struct {
 	// Connection upgrader for upgrading insecure stream connections to
 	// secure multiplex connections.
 	upgrader transport.Upgrader
 
-	// Explicitly disable reuseport.
-	disableReuseport bool
+	disableReuseport bool // Explicitly disable reuseport.
+	enableMetrics    bool
 
 	// TCP connect timeout
 	connectTimeout time.Duration
@@ -189,10 +196,14 @@ func (t *TcpTransport) Dial(ctx context.Context, raddr ma.Multiaddr, p peer.ID)
 	// This means we can immediately reuse the 5-tuple and reconnect.
 	tryLinger(conn, 0)
 	tryKeepAlive(conn, true)
-	c, err := newTracingConn(conn, true)
-	if err != nil {
-		connScope.Done()
-		return nil, err
+	c := conn
+	if t.enableMetrics {
+		var err error
+		c, err = newTracingConn(conn, true)
+		if err != nil {
+			connScope.Done()
+			return nil, err
+		}
 	}
 	direction := network.DirOutbound
 	if ok, isClient, _ := network.GetSimultaneousConnect(ctx); ok && !isClient {
@@ -219,7 +230,9 @@ func (t *TcpTransport) Listen(laddr ma.Multiaddr) (transport.Listener, error) {
 	if err != nil {
 		return nil, err
 	}
-	list = newTracingListener(&tcpListener{list, 0})
+	if t.enableMetrics {
+		list = newTracingListener(&tcpListener{list, 0})
+	}
 	return t.upgrader.UpgradeListener(t, list), nil
 }
 
diff --git a/p2p/transport/websocket/addrs.go b/p2p/transport/websocket/addrs.go
index 608eb2d0..fed649dc 100644
--- a/p2p/transport/websocket/addrs.go
+++ b/p2p/transport/websocket/addrs.go
@@ -105,23 +105,17 @@ func ParseWebsocketNetAddr(a net.Addr) (ma.Multiaddr, error) {
 }
 
 func parseMultiaddr(maddr ma.Multiaddr) (*url.URL, error) {
-	// Only look at the _last_ component.
-	maddr, wscomponent := ma.SplitLast(maddr)
-	if maddr == nil || wscomponent == nil {
-		return nil, fmt.Errorf("websocket addrs need at least two components")
+	parsed, err := parseWebsocketMultiaddr(maddr)
+	if err != nil {
+		return nil, err
 	}
 
-	var scheme string
-	switch wscomponent.Protocol().Code {
-	case ma.P_WS:
-		scheme = "ws"
-	case ma.P_WSS:
+	scheme := "ws"
+	if parsed.isWSS {
 		scheme = "wss"
-	default:
-		return nil, fmt.Errorf("not a websocket multiaddr")
 	}
 
-	network, host, err := manet.DialArgs(maddr)
+	network, host, err := manet.DialArgs(parsed.restMultiaddr)
 	if err != nil {
 		return nil, err
 	}
@@ -135,3 +129,47 @@ func parseMultiaddr(maddr ma.Multiaddr) (*url.URL, error) {
 		Host:   host,
 	}, nil
 }
+
+type parsedWebsocketMultiaddr struct {
+	isWSS bool
+	// sni is the SNI value for the TLS handshake
+	sni *ma.Component
+	// the rest of the multiaddr before the /tls/sni/example.com/ws or /ws or /wss
+	restMultiaddr ma.Multiaddr
+}
+
+func parseWebsocketMultiaddr(a ma.Multiaddr) (parsedWebsocketMultiaddr, error) {
+	out := parsedWebsocketMultiaddr{}
+	// First check if we have a WSS component. If so we'll canonicalize it into a /tls/ws
+	withoutWss := a.Decapsulate(wssComponent)
+	if !withoutWss.Equal(a) {
+		a = withoutWss.Encapsulate(tlsWsComponent)
+	}
+
+	// Remove the ws component
+	withoutWs := a.Decapsulate(wsComponent)
+	if withoutWs.Equal(a) {
+		return out, fmt.Errorf("not a websocket multiaddr")
+	}
+
+	rest := withoutWs
+	// If this is not a wss then withoutWs is the rest of the multiaddr
+	out.restMultiaddr = withoutWs
+	for {
+		var head *ma.Component
+		rest, head = ma.SplitLast(rest)
+		if head == nil || rest == nil {
+			break
+		}
+
+		if head.Protocol().Code == ma.P_SNI {
+			out.sni = head
+		} else if head.Protocol().Code == ma.P_TLS {
+			out.isWSS = true
+			out.restMultiaddr = rest
+			break
+		}
+	}
+
+	return out, nil
+}
diff --git a/p2p/transport/websocket/listener.go b/p2p/transport/websocket/listener.go
index b94bed79..128fdf5e 100644
--- a/p2p/transport/websocket/listener.go
+++ b/p2p/transport/websocket/listener.go
@@ -10,11 +10,6 @@ import (
 	manet "github.com/multiformats/go-multiaddr/net"
 )
 
-var (
-	wsma  = ma.StringCast("/ws")
-	wssma = ma.StringCast("/wss")
-)
-
 type listener struct {
 	nl     net.Listener
 	server http.Server
@@ -25,16 +20,31 @@ type listener struct {
 	incoming chan *Conn
 }
 
+func (pwma *parsedWebsocketMultiaddr) toMultiaddr() ma.Multiaddr {
+	if !pwma.isWSS {
+		return pwma.restMultiaddr.Encapsulate(wsComponent)
+	}
+
+	if pwma.sni == nil {
+		return pwma.restMultiaddr.Encapsulate(tlsComponent).Encapsulate(wsComponent)
+	}
+
+	return pwma.restMultiaddr.Encapsulate(tlsComponent).Encapsulate(pwma.sni).Encapsulate(wsComponent)
+}
+
 // newListener creates a new listener from a raw net.Listener.
 // tlsConf may be nil (for unencrypted websockets).
 func newListener(a ma.Multiaddr, tlsConf *tls.Config) (*listener, error) {
-	// Only look at the _last_ component.
-	maddr, wscomponent := ma.SplitLast(a)
-	isWSS := wscomponent.Equal(wssma)
-	if isWSS && tlsConf == nil {
+	parsed, err := parseWebsocketMultiaddr(a)
+	if err != nil {
+		return nil, err
+	}
+
+	if parsed.isWSS && tlsConf == nil {
 		return nil, fmt.Errorf("cannot listen on wss address %s without a tls.Config", a)
 	}
-	lnet, lnaddr, err := manet.DialArgs(maddr)
+
+	lnet, lnaddr, err := manet.DialArgs(parsed.restMultiaddr)
 	if err != nil {
 		return nil, err
 	}
@@ -54,15 +64,16 @@ func newListener(a ma.Multiaddr, tlsConf *tls.Config) (*listener, error) {
 		_, last := ma.SplitFirst(laddr)
 		laddr = first.Encapsulate(last)
 	}
+	parsed.restMultiaddr = laddr
 
 	ln := &listener{
 		nl:       nl,
-		laddr:    laddr.Encapsulate(wscomponent),
+		laddr:    parsed.toMultiaddr(),
 		incoming: make(chan *Conn),
 		closed:   make(chan struct{}),
 	}
 	ln.server = http.Server{Handler: ln}
-	if isWSS {
+	if parsed.isWSS {
 		ln.server.TLSConfig = tlsConf
 	}
 	return ln, nil
diff --git a/p2p/transport/websocket/websocket.go b/p2p/transport/websocket/websocket.go
index 42c4c618..04af9b8c 100644
--- a/p2p/transport/websocket/websocket.go
+++ b/p2p/transport/websocket/websocket.go
@@ -21,9 +21,27 @@ import (
 // WsFmt is multiaddr formatter for WsProtocol
 var WsFmt = mafmt.And(mafmt.TCP, mafmt.Base(ma.P_WS))
 
-// This is _not_ WsFmt because we want the transport to stick to dialing fully
-// resolved addresses.
-var dialMatcher = mafmt.And(mafmt.Or(mafmt.IP, mafmt.DNS), mafmt.Base(ma.P_TCP), mafmt.Or(mafmt.Base(ma.P_WS), mafmt.Base(ma.P_WSS)))
+var dialMatcher = mafmt.And(
+	mafmt.Or(mafmt.IP, mafmt.DNS),
+	mafmt.Base(ma.P_TCP),
+	mafmt.Or(
+		mafmt.Base(ma.P_WS),
+		mafmt.And(
+			mafmt.Or(
+				mafmt.And(
+					mafmt.Base(ma.P_TLS),
+					mafmt.Base(ma.P_SNI)),
+				mafmt.Base(ma.P_TLS),
+			),
+			mafmt.Base(ma.P_WS)),
+		mafmt.Base(ma.P_WSS)))
+
+var (
+	wssComponent   = ma.StringCast("/wss")
+	tlsWsComponent = ma.StringCast("/tls/ws")
+	tlsComponent   = ma.StringCast("/tls")
+	wsComponent    = ma.StringCast("/ws")
+)
 
 func init() {
 	manet.RegisterFromNetAddr(ParseWebsocketNetAddr, "websocket")
@@ -100,6 +118,42 @@ func (t *WebsocketTransport) Proxy() bool {
 	return false
 }
 
+func (t *WebsocketTransport) Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multiaddr, error) {
+	parsed, err := parseWebsocketMultiaddr(maddr)
+	if err != nil {
+		return nil, err
+	}
+
+	if !parsed.isWSS {
+		// No /tls/ws component, this isn't a secure websocket multiaddr. We can just return it here
+		return []ma.Multiaddr{maddr}, nil
+	}
+
+	if parsed.sni == nil {
+		var err error
+		// We don't have an sni component, we'll use dns/dnsaddr
+		ma.ForEach(parsed.restMultiaddr, func(c ma.Component) bool {
+			switch c.Protocol().Code {
+			case ma.P_DNS, ma.P_DNS4, ma.P_DNS6, ma.P_DNSADDR:
+				// err shouldn't happen since this means we couldn't parse a dns hostname for an sni value.
+				parsed.sni, err = ma.NewComponent("sni", c.Value())
+				return false
+			}
+			return true
+		})
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if parsed.sni == nil {
+		// we didn't find anything to set the sni with. So we just return the given multiaddr
+		return []ma.Multiaddr{maddr}, nil
+	}
+
+	return []ma.Multiaddr{parsed.toMultiaddr()}, nil
+}
+
 func (t *WebsocketTransport) Dial(ctx context.Context, raddr ma.Multiaddr, p peer.ID) (transport.CapableConn, error) {
 	connScope, err := t.rcmgr.OpenConnection(network.DirOutbound, true, raddr)
 	if err != nil {
@@ -121,9 +175,21 @@ func (t *WebsocketTransport) maDial(ctx context.Context, raddr ma.Multiaddr) (ma
 	isWss := wsurl.Scheme == "wss"
 	dialer := ws.Dialer{HandshakeTimeout: 30 * time.Second}
 	if isWss {
-		dialer.TLSClientConfig = t.tlsClientConf
+		sni := ""
+		sni, err = raddr.ValueForProtocol(ma.P_SNI)
+		if err != nil {
+			sni = ""
+		}
 
+		if sni != "" {
+			copytlsClientConf := t.tlsClientConf.Clone()
+			copytlsClientConf.ServerName = sni
+			dialer.TLSClientConfig = copytlsClientConf
+		} else {
+			dialer.TLSClientConfig = t.tlsClientConf
+		}
 	}
+
 	wscon, _, err := dialer.DialContext(ctx, wsurl.String(), nil)
 	if err != nil {
 		return nil, err
diff --git a/p2p/transport/webtransport/cert_manager.go b/p2p/transport/webtransport/cert_manager.go
new file mode 100644
index 00000000..c005d448
--- /dev/null
+++ b/p2p/transport/webtransport/cert_manager.go
@@ -0,0 +1,168 @@
+package libp2pwebtransport
+
+import (
+	"bytes"
+	"context"
+	"crypto/sha256"
+	"crypto/tls"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/benbjohnson/clock"
+	ma "github.com/multiformats/go-multiaddr"
+	"github.com/multiformats/go-multihash"
+)
+
+// Allow for a bit of clock skew.
+// 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
+
+type certConfig struct {
+	tlsConf *tls.Config
+	sha256  [32]byte // cached from the tlsConf
+}
+
+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)
+	if err != nil {
+		return nil, err
+	}
+	return &certConfig{
+		tlsConf: conf,
+		sha256:  sha256.Sum256(conf.Certificates[0].Leaf.Raw),
+	}, nil
+}
+
+// Certificate renewal logic:
+//  1. On startup, we generate one cert that is valid from now (-1h, to allow for clock skew), and another
+//     cert that is valid from the expiry date of the first certificate (again, with allowance for clock skew).
+//  2. Once we reach 1h before expiry of the first certificate, we switch over to the second certificate.
+//     At the same time, we stop advertising the certhash of the first cert and generate the next cert.
+type certManager struct {
+	clock     clock.Clock
+	ctx       context.Context
+	ctxCancel context.CancelFunc
+	refCount  sync.WaitGroup
+
+	mx            sync.RWMutex
+	lastConfig    *certConfig // initially nil
+	currentConfig *certConfig
+	nextConfig    *certConfig // nil until we have passed half the certValidity of the current config
+	addrComp      ma.Multiaddr
+}
+
+func newCertManager(clock clock.Clock) (*certManager, error) {
+	m := &certManager{clock: clock}
+	m.ctx, m.ctxCancel = context.WithCancel(context.Background())
+	if err := m.init(); err != nil {
+		return nil, err
+	}
+
+	m.background()
+	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))
+	if err != nil {
+		return err
+	}
+	return m.rollConfig()
+}
+
+func (m *certManager) rollConfig() 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))
+	if err != nil {
+		return err
+	}
+	m.lastConfig = m.currentConfig
+	m.currentConfig = m.nextConfig
+	m.nextConfig = c
+	return m.cacheAddrComponent()
+}
+
+func (m *certManager) background() {
+	d := m.currentConfig.End().Add(-clockSkewAllowance).Sub(m.clock.Now())
+	log.Debugw("setting timer", "duration", d.String())
+	t := m.clock.Timer(d)
+	m.refCount.Add(1)
+
+	go func() {
+		defer m.refCount.Done()
+		defer t.Stop()
+
+		for {
+			select {
+			case <-m.ctx.Done():
+				return
+			case now := <-t.C:
+				m.mx.Lock()
+				if err := m.rollConfig(); err != nil {
+					log.Errorw("rolling config failed", "error", err)
+				}
+				d := m.currentConfig.End().Add(-clockSkewAllowance).Sub(now)
+				log.Debugw("rolling certificates", "next", d.String())
+				t.Reset(d)
+				m.mx.Unlock()
+			}
+		}
+	}()
+}
+
+func (m *certManager) GetConfig() *tls.Config {
+	m.mx.RLock()
+	defer m.mx.RUnlock()
+	return m.currentConfig.tlsConf
+}
+
+func (m *certManager) AddrComponent() ma.Multiaddr {
+	m.mx.RLock()
+	defer m.mx.RUnlock()
+	return m.addrComp
+}
+
+func (m *certManager) Verify(hashes []multihash.DecodedMultihash) error {
+	for _, h := range hashes {
+		if h.Code != multihash.SHA2_256 {
+			return fmt.Errorf("expected SHA256 hash, got %d", h.Code)
+		}
+		if !bytes.Equal(h.Digest, m.currentConfig.sha256[:]) &&
+			(m.nextConfig == nil || !bytes.Equal(h.Digest, m.nextConfig.sha256[:])) &&
+			(m.lastConfig == nil || !bytes.Equal(h.Digest, m.lastConfig.sha256[:])) {
+			return fmt.Errorf("found unexpected hash: %+x", h.Digest)
+		}
+	}
+	return nil
+}
+
+func (m *certManager) cacheAddrComponent() error {
+	addr, err := addrComponentForCert(m.currentConfig.sha256[:])
+	if err != nil {
+		return err
+	}
+	if m.nextConfig != nil {
+		comp, err := addrComponentForCert(m.nextConfig.sha256[:])
+		if err != nil {
+			return err
+		}
+		addr = addr.Encapsulate(comp)
+	}
+	m.addrComp = addr
+	return nil
+}
+
+func (m *certManager) Close() error {
+	m.ctxCancel()
+	m.refCount.Wait()
+	return nil
+}
diff --git a/p2p/transport/webtransport/conn.go b/p2p/transport/webtransport/conn.go
new file mode 100644
index 00000000..e4d30e4a
--- /dev/null
+++ b/p2p/transport/webtransport/conn.go
@@ -0,0 +1,60 @@
+package libp2pwebtransport
+
+import (
+	"context"
+
+	"github.com/libp2p/go-libp2p/core/network"
+	tpt "github.com/libp2p/go-libp2p/core/transport"
+
+	"github.com/marten-seemann/webtransport-go"
+	ma "github.com/multiformats/go-multiaddr"
+)
+
+type connSecurityMultiaddrs struct {
+	network.ConnSecurity
+	network.ConnMultiaddrs
+}
+
+type connMultiaddrs struct {
+	local, remote ma.Multiaddr
+}
+
+var _ network.ConnMultiaddrs = &connMultiaddrs{}
+
+func (c *connMultiaddrs) LocalMultiaddr() ma.Multiaddr  { return c.local }
+func (c *connMultiaddrs) RemoteMultiaddr() ma.Multiaddr { return c.remote }
+
+type conn struct {
+	*connSecurityMultiaddrs
+
+	transport tpt.Transport
+	session   *webtransport.Session
+
+	scope network.ConnScope
+}
+
+var _ tpt.CapableConn = &conn{}
+
+func newConn(tr tpt.Transport, sess *webtransport.Session, sconn *connSecurityMultiaddrs, scope network.ConnScope) *conn {
+	return &conn{
+		connSecurityMultiaddrs: sconn,
+		transport:              tr,
+		session:                sess,
+		scope:                  scope,
+	}
+}
+
+func (c *conn) OpenStream(ctx context.Context) (network.MuxedStream, error) {
+	str, err := c.session.OpenStreamSync(ctx)
+	return &stream{str}, err
+}
+
+func (c *conn) AcceptStream() (network.MuxedStream, error) {
+	str, err := c.session.AcceptStream(context.Background())
+	return &stream{str}, err
+}
+
+func (c *conn) Close() error             { return c.session.Close() }
+func (c *conn) IsClosed() bool           { return c.session.Context().Err() != nil }
+func (c *conn) Scope() network.ConnScope { return c.scope }
+func (c *conn) Transport() tpt.Transport { return c.transport }
diff --git a/p2p/transport/webtransport/crypto.go b/p2p/transport/webtransport/crypto.go
new file mode 100644
index 00000000..0ea71323
--- /dev/null
+++ b/p2p/transport/webtransport/crypto.go
@@ -0,0 +1,108 @@
+package libp2pwebtransport
+
+import (
+	"bytes"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/sha256"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"math/big"
+	"time"
+
+	"github.com/multiformats/go-multihash"
+)
+
+func getTLSConf(start, end time.Time) (*tls.Config, error) {
+	cert, priv, err := generateCert(start, end)
+	if err != nil {
+		return nil, err
+	}
+	return &tls.Config{
+		Certificates: []tls.Certificate{{
+			Certificate: [][]byte{cert.Raw},
+			PrivateKey:  priv,
+			Leaf:        cert,
+		}},
+	}, nil
+}
+
+func generateCert(start, end time.Time) (*x509.Certificate, *ecdsa.PrivateKey, error) {
+	b := make([]byte, 8)
+	if _, err := rand.Read(b); err != nil {
+		return nil, nil, err
+	}
+	serial := int64(binary.BigEndian.Uint64(b))
+	if serial < 0 {
+		serial = -serial
+	}
+	certTempl := &x509.Certificate{
+		SerialNumber:          big.NewInt(serial),
+		Subject:               pkix.Name{},
+		NotBefore:             start,
+		NotAfter:              end,
+		IsCA:                  true,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		BasicConstraintsValid: true,
+	}
+	caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return nil, nil, err
+	}
+	caBytes, err := x509.CreateCertificate(rand.Reader, certTempl, certTempl, &caPrivateKey.PublicKey, caPrivateKey)
+	if err != nil {
+		return nil, nil, err
+	}
+	ca, err := x509.ParseCertificate(caBytes)
+	if err != nil {
+		return nil, nil, err
+	}
+	return ca, caPrivateKey, nil
+}
+
+func verifyRawCerts(rawCerts [][]byte, certHashes []multihash.DecodedMultihash) error {
+	if len(rawCerts) < 1 {
+		return errors.New("no cert")
+	}
+	leaf := rawCerts[len(rawCerts)-1]
+	// The W3C WebTransport specification currently only allows SHA-256 certificates for serverCertificateHashes.
+	hash := sha256.Sum256(leaf)
+	var verified bool
+	for _, h := range certHashes {
+		if h.Code == multihash.SHA2_256 && bytes.Equal(h.Digest, hash[:]) {
+			verified = true
+			break
+		}
+	}
+	if !verified {
+		digests := make([][]byte, 0, len(certHashes))
+		for _, h := range certHashes {
+			digests = append(digests, h.Digest)
+		}
+		return fmt.Errorf("cert hash not found: %#x (expected: %#x)", hash, digests)
+	}
+
+	cert, err := x509.ParseCertificate(leaf)
+	if err != nil {
+		return err
+	}
+	// TODO: is this the best (and complete?) way to identify RSA certificates?
+	switch cert.SignatureAlgorithm {
+	case x509.SHA1WithRSA, x509.SHA256WithRSA, x509.SHA384WithRSA, x509.SHA512WithRSA, x509.MD2WithRSA, x509.MD5WithRSA:
+		return errors.New("cert uses RSA")
+	}
+	if l := cert.NotAfter.Sub(cert.NotBefore); l > 14*24*time.Hour {
+		return fmt.Errorf("cert must not be valid for longer than 14 days (NotBefore: %s, NotAfter: %s, Length: %s)", cert.NotBefore, cert.NotAfter, l)
+	}
+	now := time.Now()
+	if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
+		return fmt.Errorf("cert not valid (NotBefore: %s, NotAfter: %s)", cert.NotBefore, cert.NotAfter)
+	}
+	return nil
+}
diff --git a/p2p/transport/webtransport/listener.go b/p2p/transport/webtransport/listener.go
new file mode 100644
index 00000000..bf0405a4
--- /dev/null
+++ b/p2p/transport/webtransport/listener.go
@@ -0,0 +1,256 @@
+package libp2pwebtransport
+
+import (
+	"context"
+	"crypto/tls"
+	"errors"
+	"fmt"
+	"net"
+	"net/http"
+	"time"
+
+	"github.com/libp2p/go-libp2p/core/connmgr"
+	"github.com/libp2p/go-libp2p/core/network"
+	tpt "github.com/libp2p/go-libp2p/core/transport"
+	"github.com/libp2p/go-libp2p/p2p/security/noise"
+	pb "github.com/libp2p/go-libp2p/p2p/transport/webtransport/pb"
+
+	"github.com/lucas-clemente/quic-go/http3"
+	"github.com/marten-seemann/webtransport-go"
+	ma "github.com/multiformats/go-multiaddr"
+	manet "github.com/multiformats/go-multiaddr/net"
+	"github.com/multiformats/go-multihash"
+)
+
+var errClosed = errors.New("closed")
+
+const queueLen = 16
+const handshakeTimeout = 10 * time.Second
+
+type listener struct {
+	transport       tpt.Transport
+	noise           *noise.Transport
+	certManager     *certManager
+	tlsConf         *tls.Config
+	isStaticTLSConf bool
+
+	rcmgr network.ResourceManager
+	gater connmgr.ConnectionGater
+
+	server webtransport.Server
+
+	ctx       context.Context
+	ctxCancel context.CancelFunc
+
+	serverClosed chan struct{} // is closed when server.Serve returns
+
+	addr      net.Addr
+	multiaddr ma.Multiaddr
+
+	queue chan tpt.CapableConn
+}
+
+var _ tpt.Listener = &listener{}
+
+func newListener(laddr ma.Multiaddr, transport tpt.Transport, noise *noise.Transport, certManager *certManager, tlsConf *tls.Config, gater connmgr.ConnectionGater, rcmgr network.ResourceManager) (tpt.Listener, error) {
+	network, addr, err := manet.DialArgs(laddr)
+	if err != nil {
+		return nil, err
+	}
+	udpAddr, err := net.ResolveUDPAddr(network, addr)
+	if err != nil {
+		return nil, err
+	}
+	udpConn, err := net.ListenUDP(network, udpAddr)
+	if err != nil {
+		return nil, err
+	}
+	localMultiaddr, err := toWebtransportMultiaddr(udpConn.LocalAddr())
+	if err != nil {
+		return nil, err
+	}
+	isStaticTLSConf := tlsConf != nil
+	if tlsConf == nil {
+		tlsConf = &tls.Config{GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
+			return certManager.GetConfig(), nil
+		}}
+	}
+	ln := &listener{
+		transport:       transport,
+		noise:           noise,
+		certManager:     certManager,
+		tlsConf:         tlsConf,
+		isStaticTLSConf: isStaticTLSConf,
+		rcmgr:           rcmgr,
+		gater:           gater,
+		queue:           make(chan tpt.CapableConn, queueLen),
+		serverClosed:    make(chan struct{}),
+		addr:            udpConn.LocalAddr(),
+		multiaddr:       localMultiaddr,
+		server: webtransport.Server{
+			H3:          http3.Server{TLSConfig: tlsConf},
+			CheckOrigin: func(r *http.Request) bool { return true },
+		},
+	}
+	ln.ctx, ln.ctxCancel = context.WithCancel(context.Background())
+	mux := http.NewServeMux()
+	mux.HandleFunc(webtransportHTTPEndpoint, ln.httpHandler)
+	ln.server.H3.Handler = mux
+	go func() {
+		defer close(ln.serverClosed)
+		defer func() { udpConn.Close() }()
+		if err := ln.server.Serve(udpConn); err != nil {
+			// TODO: only output if the server hasn't been closed
+			log.Debugw("serving failed", "addr", udpConn.LocalAddr(), "error", err)
+		}
+	}()
+	return ln, nil
+}
+
+func (l *listener) httpHandler(w http.ResponseWriter, r *http.Request) {
+	typ, ok := r.URL.Query()["type"]
+	if !ok || len(typ) != 1 || typ[0] != "noise" {
+		w.WriteHeader(http.StatusBadRequest)
+		return
+	}
+	remoteMultiaddr, err := stringToWebtransportMultiaddr(r.RemoteAddr)
+	if err != nil {
+		// This should never happen.
+		log.Errorw("converting remote address failed", "remote", r.RemoteAddr, "error", err)
+		w.WriteHeader(http.StatusBadRequest)
+		return
+	}
+	if l.gater != nil && !l.gater.InterceptAccept(&connMultiaddrs{local: l.multiaddr, remote: remoteMultiaddr}) {
+		w.WriteHeader(http.StatusForbidden)
+		return
+	}
+
+	connScope, err := l.rcmgr.OpenConnection(network.DirInbound, false, remoteMultiaddr)
+	if err != nil {
+		log.Debugw("resource manager blocked incoming connection", "addr", r.RemoteAddr, "error", err)
+		w.WriteHeader(http.StatusServiceUnavailable)
+		return
+	}
+
+	sess, err := l.server.Upgrade(w, r)
+	if err != nil {
+		log.Debugw("upgrade failed", "error", err)
+		// TODO: think about the status code to use here
+		w.WriteHeader(500)
+		connScope.Done()
+		return
+	}
+	ctx, cancel := context.WithTimeout(l.ctx, handshakeTimeout)
+	sconn, err := l.handshake(ctx, sess)
+	if err != nil {
+		cancel()
+		log.Debugw("handshake failed", "error", err)
+		sess.Close()
+		connScope.Done()
+		return
+	}
+	cancel()
+
+	if l.gater != nil && !l.gater.InterceptSecured(network.DirInbound, sconn.RemotePeer(), sconn) {
+		// TODO: can we close with a specific error here?
+		sess.Close()
+		connScope.Done()
+		return
+	}
+
+	if err := connScope.SetPeer(sconn.RemotePeer()); err != nil {
+		log.Debugw("resource manager blocked incoming connection for peer", "peer", sconn.RemotePeer(), "addr", r.RemoteAddr, "error", err)
+		sess.Close()
+		connScope.Done()
+		return
+	}
+
+	select {
+	case l.queue <- newConn(l.transport, sess, sconn, connScope):
+	default:
+		log.Debugw("accept queue full, dropping incoming connection", "peer", sconn.RemotePeer(), "addr", r.RemoteAddr, "error", err)
+		sess.Close()
+		connScope.Done()
+	}
+}
+
+func (l *listener) Accept() (tpt.CapableConn, error) {
+	select {
+	case <-l.ctx.Done():
+		return nil, errClosed
+	case c := <-l.queue:
+		return c, nil
+	}
+}
+
+func (l *listener) handshake(ctx context.Context, sess *webtransport.Session) (*connSecurityMultiaddrs, error) {
+	local, err := toWebtransportMultiaddr(sess.LocalAddr())
+	if err != nil {
+		return nil, fmt.Errorf("error determiniting local addr: %w", err)
+	}
+	remote, err := toWebtransportMultiaddr(sess.RemoteAddr())
+	if err != nil {
+		return nil, fmt.Errorf("error determiniting remote addr: %w", err)
+	}
+
+	str, err := sess.AcceptStream(ctx)
+	if err != nil {
+		return nil, err
+	}
+	n, err := l.noise.WithSessionOptions(noise.EarlyData(newEarlyDataReceiver(l.checkEarlyData)))
+	if err != nil {
+		return nil, fmt.Errorf("failed to initialize Noise session: %w", err)
+	}
+	c, err := n.SecureInbound(ctx, &webtransportStream{Stream: str, wsess: sess}, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return &connSecurityMultiaddrs{
+		ConnSecurity:   c,
+		ConnMultiaddrs: &connMultiaddrs{local: local, remote: remote},
+	}, nil
+}
+
+func (l *listener) checkEarlyData(b []byte) error {
+	var msg pb.WebTransport
+	if err := msg.Unmarshal(b); err != nil {
+		fmt.Println(1)
+		return fmt.Errorf("failed to unmarshal early data protobuf: %w", err)
+	}
+
+	if l.isStaticTLSConf {
+		if len(msg.CertHashes) > 0 {
+			return errors.New("using static TLS config, didn't expect any certificate hashes")
+		}
+		return nil
+	}
+
+	hashes := make([]multihash.DecodedMultihash, 0, len(msg.CertHashes))
+	for _, h := range msg.CertHashes {
+		dh, err := multihash.Decode(h)
+		if err != nil {
+			return fmt.Errorf("failed to decode hash: %w", err)
+		}
+		hashes = append(hashes, *dh)
+	}
+	return l.certManager.Verify(hashes)
+}
+
+func (l *listener) Addr() net.Addr {
+	return l.addr
+}
+
+func (l *listener) Multiaddr() ma.Multiaddr {
+	if l.certManager == nil {
+		return l.multiaddr
+	}
+	return l.multiaddr.Encapsulate(l.certManager.AddrComponent())
+}
+
+func (l *listener) Close() error {
+	l.ctxCancel()
+	err := l.server.Close()
+	<-l.serverClosed
+	return err
+}
diff --git a/p2p/transport/webtransport/multiaddr.go b/p2p/transport/webtransport/multiaddr.go
new file mode 100644
index 00000000..7789a3d1
--- /dev/null
+++ b/p2p/transport/webtransport/multiaddr.go
@@ -0,0 +1,80 @@
+package libp2pwebtransport
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"strconv"
+
+	ma "github.com/multiformats/go-multiaddr"
+	mafmt "github.com/multiformats/go-multiaddr-fmt"
+	manet "github.com/multiformats/go-multiaddr/net"
+	"github.com/multiformats/go-multibase"
+	"github.com/multiformats/go-multihash"
+)
+
+var webtransportMA = ma.StringCast("/quic/webtransport")
+
+var webtransportMatcher = mafmt.And(mafmt.IP, mafmt.Base(ma.P_UDP), mafmt.Base(ma.P_QUIC), mafmt.Base(ma.P_WEBTRANSPORT))
+
+func toWebtransportMultiaddr(na net.Addr) (ma.Multiaddr, error) {
+	addr, err := manet.FromNetAddr(na)
+	if err != nil {
+		return nil, err
+	}
+	if _, err := addr.ValueForProtocol(ma.P_UDP); err != nil {
+		return nil, errors.New("not a UDP address")
+	}
+	return addr.Encapsulate(webtransportMA), nil
+}
+
+func stringToWebtransportMultiaddr(str string) (ma.Multiaddr, error) {
+	host, portStr, err := net.SplitHostPort(str)
+	if err != nil {
+		return nil, err
+	}
+	port, err := strconv.ParseInt(portStr, 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	ip := net.ParseIP(host)
+	if ip == nil {
+		return nil, errors.New("failed to parse IP")
+	}
+	return toWebtransportMultiaddr(&net.UDPAddr{IP: ip, Port: int(port)})
+}
+
+func extractCertHashes(addr ma.Multiaddr) ([]multihash.DecodedMultihash, error) {
+	certHashesStr := make([]string, 0, 2)
+	ma.ForEach(addr, func(c ma.Component) bool {
+		if c.Protocol().Code == ma.P_CERTHASH {
+			certHashesStr = append(certHashesStr, c.Value())
+		}
+		return true
+	})
+	certHashes := make([]multihash.DecodedMultihash, 0, len(certHashesStr))
+	for _, s := range certHashesStr {
+		_, ch, err := multibase.Decode(s)
+		if err != nil {
+			return nil, fmt.Errorf("failed to multibase-decode certificate hash: %w", err)
+		}
+		dh, err := multihash.Decode(ch)
+		if err != nil {
+			return nil, fmt.Errorf("failed to multihash-decode certificate hash: %w", err)
+		}
+		certHashes = append(certHashes, *dh)
+	}
+	return certHashes, nil
+}
+
+func addrComponentForCert(hash []byte) (ma.Multiaddr, error) {
+	mh, err := multihash.Encode(hash, multihash.SHA2_256)
+	if err != nil {
+		return nil, err
+	}
+	certStr, err := multibase.Encode(multibase.Base58BTC, mh)
+	if err != nil {
+		return nil, err
+	}
+	return ma.NewComponent(ma.ProtocolWithCode(ma.P_CERTHASH).Name, certStr)
+}
diff --git a/p2p/transport/webtransport/noise_early_data.go b/p2p/transport/webtransport/noise_early_data.go
new file mode 100644
index 00000000..ec01c6d7
--- /dev/null
+++ b/p2p/transport/webtransport/noise_early_data.go
@@ -0,0 +1,34 @@
+package libp2pwebtransport
+
+import (
+	"context"
+	"github.com/libp2p/go-libp2p/core/peer"
+	"github.com/libp2p/go-libp2p/p2p/security/noise"
+	"net"
+)
+
+type earlyDataHandler struct {
+	earlyData []byte
+	receive   func([]byte) error
+}
+
+var _ noise.EarlyDataHandler = &earlyDataHandler{}
+
+func newEarlyDataSender(earlyData []byte) noise.EarlyDataHandler {
+	return &earlyDataHandler{earlyData: earlyData}
+}
+
+func newEarlyDataReceiver(receive func([]byte) error) noise.EarlyDataHandler {
+	return &earlyDataHandler{receive: receive}
+}
+
+func (e *earlyDataHandler) Send(context.Context, net.Conn, peer.ID) []byte {
+	return e.earlyData
+}
+
+func (e *earlyDataHandler) Received(_ context.Context, _ net.Conn, b []byte) error {
+	if e.receive == nil {
+		return nil
+	}
+	return e.receive(b)
+}
diff --git a/p2p/transport/webtransport/pb/Makefile b/p2p/transport/webtransport/pb/Makefile
new file mode 100644
index 00000000..8af2dd81
--- /dev/null
+++ b/p2p/transport/webtransport/pb/Makefile
@@ -0,0 +1,11 @@
+PB = $(wildcard *.proto)
+GO = $(PB:.proto=.pb.go)
+
+all: $(GO)
+
+%.pb.go: %.proto
+		protoc --proto_path=$(PWD)/../..:. --gogofaster_out=. $<
+
+clean:
+		rm -f *.pb.go
+		rm -f *.go
diff --git a/p2p/transport/webtransport/pb/webtransport.pb.go b/p2p/transport/webtransport/pb/webtransport.pb.go
new file mode 100644
index 00000000..810d0667
--- /dev/null
+++ b/p2p/transport/webtransport/pb/webtransport.pb.go
@@ -0,0 +1,315 @@
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
+// source: webtransport.proto
+
+package webtransport
+
+import (
+	fmt "fmt"
+	proto "github.com/gogo/protobuf/proto"
+	io "io"
+	math "math"
+	math_bits "math/bits"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
+
+type WebTransport struct {
+	CertHashes [][]byte `protobuf:"bytes,1,rep,name=cert_hashes,json=certHashes" json:"cert_hashes,omitempty"`
+}
+
+func (m *WebTransport) Reset()         { *m = WebTransport{} }
+func (m *WebTransport) String() string { return proto.CompactTextString(m) }
+func (*WebTransport) ProtoMessage()    {}
+func (*WebTransport) Descriptor() ([]byte, []int) {
+	return fileDescriptor_db878920ab41a4f3, []int{0}
+}
+func (m *WebTransport) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *WebTransport) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_WebTransport.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *WebTransport) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_WebTransport.Merge(m, src)
+}
+func (m *WebTransport) XXX_Size() int {
+	return m.Size()
+}
+func (m *WebTransport) XXX_DiscardUnknown() {
+	xxx_messageInfo_WebTransport.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_WebTransport proto.InternalMessageInfo
+
+func (m *WebTransport) GetCertHashes() [][]byte {
+	if m != nil {
+		return m.CertHashes
+	}
+	return nil
+}
+
+func init() {
+	proto.RegisterType((*WebTransport)(nil), "WebTransport")
+}
+
+func init() { proto.RegisterFile("webtransport.proto", fileDescriptor_db878920ab41a4f3) }
+
+var fileDescriptor_db878920ab41a4f3 = []byte{
+	// 109 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x2a, 0x4f, 0x4d, 0x2a,
+	0x29, 0x4a, 0xcc, 0x2b, 0x2e, 0xc8, 0x2f, 0x2a, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0xd2,
+	0xe7, 0xe2, 0x09, 0x4f, 0x4d, 0x0a, 0x81, 0x89, 0x0a, 0xc9, 0x73, 0x71, 0x27, 0xa7, 0x16, 0x95,
+	0xc4, 0x67, 0x24, 0x16, 0x67, 0xa4, 0x16, 0x4b, 0x30, 0x2a, 0x30, 0x6b, 0xf0, 0x04, 0x71, 0x81,
+	0x84, 0x3c, 0xc0, 0x22, 0x4e, 0x12, 0x27, 0x1e, 0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0,
+	0x91, 0x1c, 0xe3, 0x84, 0xc7, 0x72, 0x0c, 0x17, 0x1e, 0xcb, 0x31, 0xdc, 0x78, 0x2c, 0xc7, 0x00,
+	0x08, 0x00, 0x00, 0xff, 0xff, 0x50, 0x77, 0xe5, 0x52, 0x5f, 0x00, 0x00, 0x00,
+}
+
+func (m *WebTransport) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *WebTransport) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *WebTransport) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if len(m.CertHashes) > 0 {
+		for iNdEx := len(m.CertHashes) - 1; iNdEx >= 0; iNdEx-- {
+			i -= len(m.CertHashes[iNdEx])
+			copy(dAtA[i:], m.CertHashes[iNdEx])
+			i = encodeVarintWebtransport(dAtA, i, uint64(len(m.CertHashes[iNdEx])))
+			i--
+			dAtA[i] = 0xa
+		}
+	}
+	return len(dAtA) - i, nil
+}
+
+func encodeVarintWebtransport(dAtA []byte, offset int, v uint64) int {
+	offset -= sovWebtransport(v)
+	base := offset
+	for v >= 1<<7 {
+		dAtA[offset] = uint8(v&0x7f | 0x80)
+		v >>= 7
+		offset++
+	}
+	dAtA[offset] = uint8(v)
+	return base
+}
+func (m *WebTransport) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if len(m.CertHashes) > 0 {
+		for _, b := range m.CertHashes {
+			l = len(b)
+			n += 1 + l + sovWebtransport(uint64(l))
+		}
+	}
+	return n
+}
+
+func sovWebtransport(x uint64) (n int) {
+	return (math_bits.Len64(x|1) + 6) / 7
+}
+func sozWebtransport(x uint64) (n int) {
+	return sovWebtransport(uint64((x << 1) ^ uint64((int64(x) >> 63))))
+}
+func (m *WebTransport) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowWebtransport
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: WebTransport: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: WebTransport: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field CertHashes", wireType)
+			}
+			var byteLen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowWebtransport
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				byteLen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if byteLen < 0 {
+				return ErrInvalidLengthWebtransport
+			}
+			postIndex := iNdEx + byteLen
+			if postIndex < 0 {
+				return ErrInvalidLengthWebtransport
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.CertHashes = append(m.CertHashes, make([]byte, postIndex-iNdEx))
+			copy(m.CertHashes[len(m.CertHashes)-1], dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipWebtransport(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthWebtransport
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func skipWebtransport(dAtA []byte) (n int, err error) {
+	l := len(dAtA)
+	iNdEx := 0
+	depth := 0
+	for iNdEx < l {
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return 0, ErrIntOverflowWebtransport
+			}
+			if iNdEx >= l {
+				return 0, io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= (uint64(b) & 0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		wireType := int(wire & 0x7)
+		switch wireType {
+		case 0:
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return 0, ErrIntOverflowWebtransport
+				}
+				if iNdEx >= l {
+					return 0, io.ErrUnexpectedEOF
+				}
+				iNdEx++
+				if dAtA[iNdEx-1] < 0x80 {
+					break
+				}
+			}
+		case 1:
+			iNdEx += 8
+		case 2:
+			var length int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return 0, ErrIntOverflowWebtransport
+				}
+				if iNdEx >= l {
+					return 0, io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				length |= (int(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if length < 0 {
+				return 0, ErrInvalidLengthWebtransport
+			}
+			iNdEx += length
+		case 3:
+			depth++
+		case 4:
+			if depth == 0 {
+				return 0, ErrUnexpectedEndOfGroupWebtransport
+			}
+			depth--
+		case 5:
+			iNdEx += 4
+		default:
+			return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
+		}
+		if iNdEx < 0 {
+			return 0, ErrInvalidLengthWebtransport
+		}
+		if depth == 0 {
+			return iNdEx, nil
+		}
+	}
+	return 0, io.ErrUnexpectedEOF
+}
+
+var (
+	ErrInvalidLengthWebtransport        = fmt.Errorf("proto: negative length found during unmarshaling")
+	ErrIntOverflowWebtransport          = fmt.Errorf("proto: integer overflow")
+	ErrUnexpectedEndOfGroupWebtransport = fmt.Errorf("proto: unexpected end of group")
+)
diff --git a/p2p/transport/webtransport/pb/webtransport.proto b/p2p/transport/webtransport/pb/webtransport.proto
new file mode 100644
index 00000000..a9129ce6
--- /dev/null
+++ b/p2p/transport/webtransport/pb/webtransport.proto
@@ -0,0 +1,5 @@
+syntax = "proto2";
+
+message WebTransport {
+  repeated bytes cert_hashes = 1;
+}
diff --git a/p2p/transport/webtransport/stream.go b/p2p/transport/webtransport/stream.go
new file mode 100644
index 00000000..ff17b308
--- /dev/null
+++ b/p2p/transport/webtransport/stream.go
@@ -0,0 +1,71 @@
+package libp2pwebtransport
+
+import (
+	"errors"
+	"net"
+
+	"github.com/marten-seemann/webtransport-go"
+
+	"github.com/libp2p/go-libp2p/core/network"
+)
+
+const (
+	reset webtransport.ErrorCode = 0
+)
+
+type webtransportStream struct {
+	webtransport.Stream
+	wsess *webtransport.Session
+}
+
+var _ net.Conn = &webtransportStream{}
+
+func (s *webtransportStream) LocalAddr() net.Addr {
+	return s.wsess.LocalAddr()
+}
+
+func (s *webtransportStream) RemoteAddr() net.Addr {
+	return s.wsess.RemoteAddr()
+}
+
+type stream struct {
+	webtransport.Stream
+}
+
+var _ network.MuxedStream = &stream{}
+
+func (s *stream) Read(b []byte) (n int, err error) {
+	n, err = s.Stream.Read(b)
+	if err != nil && errors.Is(err, &webtransport.StreamError{}) {
+		err = network.ErrReset
+	}
+	return n, err
+}
+
+func (s *stream) Write(b []byte) (n int, err error) {
+	n, err = s.Stream.Write(b)
+	if err != nil && errors.Is(err, &webtransport.StreamError{}) {
+		err = network.ErrReset
+	}
+	return n, err
+}
+
+func (s *stream) Reset() error {
+	s.Stream.CancelRead(reset)
+	s.Stream.CancelWrite(reset)
+	return nil
+}
+
+func (s *stream) Close() error {
+	s.Stream.CancelRead(reset)
+	return s.Stream.Close()
+}
+
+func (s *stream) CloseRead() error {
+	s.Stream.CancelRead(reset)
+	return nil
+}
+
+func (s *stream) CloseWrite() error {
+	return s.Stream.Close()
+}
diff --git a/p2p/transport/webtransport/transport.go b/p2p/transport/webtransport/transport.go
new file mode 100644
index 00000000..42b5fcef
--- /dev/null
+++ b/p2p/transport/webtransport/transport.go
@@ -0,0 +1,270 @@
+package libp2pwebtransport
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"io"
+	"sync"
+	"time"
+
+	"github.com/libp2p/go-libp2p/core/connmgr"
+	ic "github.com/libp2p/go-libp2p/core/crypto"
+	"github.com/libp2p/go-libp2p/core/network"
+	"github.com/libp2p/go-libp2p/core/peer"
+	tpt "github.com/libp2p/go-libp2p/core/transport"
+	"github.com/libp2p/go-libp2p/p2p/security/noise"
+	pb "github.com/libp2p/go-libp2p/p2p/transport/webtransport/pb"
+
+	"github.com/benbjohnson/clock"
+	logging "github.com/ipfs/go-log/v2"
+	"github.com/lucas-clemente/quic-go/http3"
+	"github.com/marten-seemann/webtransport-go"
+	ma "github.com/multiformats/go-multiaddr"
+	manet "github.com/multiformats/go-multiaddr/net"
+	"github.com/multiformats/go-multihash"
+)
+
+var log = logging.Logger("webtransport")
+
+const webtransportHTTPEndpoint = "/.well-known/libp2p-webtransport"
+
+const certValidity = 14 * 24 * time.Hour
+
+type Option func(*transport) error
+
+func WithClock(cl clock.Clock) Option {
+	return func(t *transport) error {
+		t.clock = cl
+		return nil
+	}
+}
+
+// WithTLSConfig sets a tls.Config used for listening.
+// When used, the certificate from that config will be used, and no /certhash will be added to the listener's multiaddr.
+// This is most useful when running a listener that has a valid (CA-signed) certificate.
+func WithTLSConfig(c *tls.Config) Option {
+	return func(t *transport) error {
+		t.staticTLSConf = c
+		return nil
+	}
+}
+
+// WithTLSClientConfig sets a custom tls.Config used for dialing.
+// This option is most useful for setting a custom tls.Config.RootCAs certificate pool.
+// When dialing a multiaddr that contains a /certhash component, this library will set InsecureSkipVerify and
+// overwrite the VerifyPeerCertificate callback.
+func WithTLSClientConfig(c *tls.Config) Option {
+	return func(t *transport) error {
+		t.tlsClientConf = c
+		return nil
+	}
+}
+
+type transport struct {
+	privKey ic.PrivKey
+	pid     peer.ID
+	clock   clock.Clock
+
+	rcmgr network.ResourceManager
+	gater connmgr.ConnectionGater
+
+	listenOnce    sync.Once
+	listenOnceErr error
+	certManager   *certManager
+	staticTLSConf *tls.Config
+	tlsClientConf *tls.Config
+
+	noise *noise.Transport
+}
+
+var _ tpt.Transport = &transport{}
+var _ io.Closer = &transport{}
+
+func New(key ic.PrivKey, gater connmgr.ConnectionGater, rcmgr network.ResourceManager, opts ...Option) (tpt.Transport, error) {
+	id, err := peer.IDFromPrivateKey(key)
+	if err != nil {
+		return nil, err
+	}
+	t := &transport{
+		pid:     id,
+		privKey: key,
+		rcmgr:   rcmgr,
+		gater:   gater,
+		clock:   clock.New(),
+	}
+	for _, opt := range opts {
+		if err := opt(t); err != nil {
+			return nil, err
+		}
+	}
+	n, err := noise.New(key)
+	if err != nil {
+		return nil, err
+	}
+	t.noise = n
+	return t, nil
+}
+
+func (t *transport) Dial(ctx context.Context, raddr ma.Multiaddr, p peer.ID) (tpt.CapableConn, error) {
+	_, addr, err := manet.DialArgs(raddr)
+	if err != nil {
+		return nil, err
+	}
+	certHashes, err := extractCertHashes(raddr)
+	if err != nil {
+		return nil, err
+	}
+
+	scope, err := t.rcmgr.OpenConnection(network.DirOutbound, false, raddr)
+	if err != nil {
+		log.Debugw("resource manager blocked outgoing connection", "peer", p, "addr", raddr, "error", err)
+		return nil, err
+	}
+	if err := scope.SetPeer(p); err != nil {
+		log.Debugw("resource manager blocked outgoing connection for peer", "peer", p, "addr", raddr, "error", err)
+		scope.Done()
+		return nil, err
+	}
+
+	sess, err := t.dial(ctx, addr, certHashes)
+	if err != nil {
+		scope.Done()
+		return nil, err
+	}
+	sconn, err := t.upgrade(ctx, sess, p, certHashes)
+	if err != nil {
+		sess.Close()
+		scope.Done()
+		return nil, err
+	}
+	if t.gater != nil && !t.gater.InterceptSecured(network.DirOutbound, p, sconn) {
+		// TODO: can we close with a specific error here?
+		sess.Close()
+		scope.Done()
+		return nil, fmt.Errorf("secured connection gated")
+	}
+
+	return newConn(t, sess, sconn, scope), nil
+}
+
+func (t *transport) dial(ctx context.Context, addr string, certHashes []multihash.DecodedMultihash) (*webtransport.Session, error) {
+	url := fmt.Sprintf("https://%s%s?type=noise", addr, webtransportHTTPEndpoint)
+	var tlsConf *tls.Config
+	if t.tlsClientConf != nil {
+		tlsConf = t.tlsClientConf.Clone()
+	} else {
+		tlsConf = &tls.Config{}
+	}
+
+	if len(certHashes) > 0 {
+		// This is not insecure. We verify the certificate ourselves.
+		// See https://www.w3.org/TR/webtransport/#certificate-hashes.
+		tlsConf.InsecureSkipVerify = true
+		tlsConf.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
+			return verifyRawCerts(rawCerts, certHashes)
+		}
+	}
+	dialer := webtransport.Dialer{
+		RoundTripper: &http3.RoundTripper{TLSClientConfig: tlsConf},
+	}
+	rsp, sess, err := dialer.Dial(ctx, url, nil)
+	if err != nil {
+		return nil, err
+	}
+	if rsp.StatusCode < 200 || rsp.StatusCode > 299 {
+		return nil, fmt.Errorf("invalid response status code: %d", rsp.StatusCode)
+	}
+	return sess, err
+}
+
+func (t *transport) upgrade(ctx context.Context, sess *webtransport.Session, p peer.ID, certHashes []multihash.DecodedMultihash) (*connSecurityMultiaddrs, error) {
+	local, err := toWebtransportMultiaddr(sess.LocalAddr())
+	if err != nil {
+		return nil, fmt.Errorf("error determiniting local addr: %w", err)
+	}
+	remote, err := toWebtransportMultiaddr(sess.RemoteAddr())
+	if err != nil {
+		return nil, fmt.Errorf("error determiniting remote addr: %w", err)
+	}
+
+	str, err := sess.OpenStreamSync(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	// Now run a Noise handshake (using early data) and send all the certificate hashes that we would have accepted.
+	// The server will verify that it advertised all of these certificate hashes.
+	msg := pb.WebTransport{CertHashes: make([][]byte, 0, len(certHashes))}
+	for _, certHash := range certHashes {
+		h, err := multihash.Encode(certHash.Digest, certHash.Code)
+		if err != nil {
+			return nil, fmt.Errorf("failed to encode certificate hash: %w", err)
+		}
+		msg.CertHashes = append(msg.CertHashes, h)
+	}
+	msgBytes, err := msg.Marshal()
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal WebTransport protobuf: %w", err)
+	}
+	n, err := t.noise.WithSessionOptions(noise.EarlyData(newEarlyDataSender(msgBytes)))
+	if err != nil {
+		return nil, fmt.Errorf("failed to create Noise transport: %w", err)
+	}
+	c, err := n.SecureOutbound(ctx, &webtransportStream{Stream: str, wsess: sess}, p)
+	if err != nil {
+		return nil, err
+	}
+	return &connSecurityMultiaddrs{
+		ConnSecurity:   c,
+		ConnMultiaddrs: &connMultiaddrs{local: local, remote: remote},
+	}, nil
+}
+
+func (t *transport) CanDial(addr ma.Multiaddr) bool {
+	var numHashes int
+	ma.ForEach(addr, func(c ma.Component) bool {
+		if c.Protocol().Code == ma.P_CERTHASH {
+			numHashes++
+		}
+		return true
+	})
+	// Remove the /certhash components from the multiaddr.
+	// If the multiaddr doesn't contain any certhashes, the node might have a CA-signed certificate.
+	for i := 0; i < numHashes; i++ {
+		addr, _ = ma.SplitLast(addr)
+	}
+	return webtransportMatcher.Matches(addr)
+}
+
+func (t *transport) Listen(laddr ma.Multiaddr) (tpt.Listener, error) {
+	if !webtransportMatcher.Matches(laddr) {
+		return nil, fmt.Errorf("cannot listen on non-WebTransport addr: %s", laddr)
+	}
+	if t.staticTLSConf == nil {
+		t.listenOnce.Do(func() {
+			t.certManager, t.listenOnceErr = newCertManager(t.clock)
+		})
+		if t.listenOnceErr != nil {
+			return nil, t.listenOnceErr
+		}
+	}
+	return newListener(laddr, t, t.noise, t.certManager, t.staticTLSConf, t.gater, t.rcmgr)
+}
+
+func (t *transport) Protocols() []int {
+	return []int{ma.P_WEBTRANSPORT}
+}
+
+func (t *transport) Proxy() bool {
+	return false
+}
+
+func (t *transport) Close() error {
+	t.listenOnce.Do(func() {})
+	if t.certManager != nil {
+		return t.certManager.Close()
+	}
+	return nil
+}


Test patch:
diff --git a/p2p/host/autorelay/autorelay_test.go b/p2p/host/autorelay/autorelay_test.go
index cb4d7e88..e776f1df 100644
--- a/p2p/host/autorelay/autorelay_test.go
+++ b/p2p/host/autorelay/autorelay_test.go
@@ -1,6 +1,7 @@
 package autorelay_test
 
 import (
+	"context"
 	"strings"
 	"sync/atomic"
 	"testing"
@@ -114,7 +115,7 @@ func newRelayV1(t *testing.T) host.Host {
 func TestSingleCandidate(t *testing.T) {
 	var counter int
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(num int) <-chan peer.AddrInfo {
+		autorelay.WithPeerSource(func(_ context.Context, num int) <-chan peer.AddrInfo {
 			counter++
 			require.Equal(t, 1, num)
 			peerChan := make(chan peer.AddrInfo, num)
@@ -148,7 +149,7 @@ func TestSingleRelay(t *testing.T) {
 	close(peerChan)
 
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(num int) <-chan peer.AddrInfo {
+		autorelay.WithPeerSource(func(_ context.Context, num int) <-chan peer.AddrInfo {
 			require.False(t, called, "expected the peer source callback to only have been called once")
 			called = true
 			require.Equal(t, numCandidates, num)
@@ -175,7 +176,7 @@ func TestPreferRelayV2(t *testing.T) {
 	})
 
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(int) <-chan peer.AddrInfo {
+		autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo {
 			peerChan := make(chan peer.AddrInfo, 1)
 			defer close(peerChan)
 			peerChan <- peer.AddrInfo{ID: r.ID(), Addrs: r.Addrs()}
@@ -193,7 +194,7 @@ func TestPreferRelayV2(t *testing.T) {
 func TestWaitForCandidates(t *testing.T) {
 	peerChan := make(chan peer.AddrInfo)
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(int) <-chan peer.AddrInfo { return peerChan }, time.Hour),
+		autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo { return peerChan }, time.Hour),
 		autorelay.WithMinCandidates(2),
 		autorelay.WithNumRelays(1),
 		autorelay.WithBootDelay(time.Hour),
@@ -239,11 +240,11 @@ func TestBackoff(t *testing.T) {
 		str.Reset()
 	})
 
-	var counter int
+	var counter int32 // to be used atomically
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(int) <-chan peer.AddrInfo {
+		autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo {
 			// always return the same node, and make sure we don't try to connect to it too frequently
-			counter++
+			atomic.AddInt32(&counter, 1)
 			peerChan := make(chan peer.AddrInfo, 1)
 			peerChan <- peer.AddrInfo{ID: r.ID(), Addrs: r.Addrs()}
 			close(peerChan)
@@ -264,7 +265,7 @@ func TestBackoff(t *testing.T) {
 	}
 	cl.Add(backoff / 2)
 	require.Eventually(t, func() bool { return atomic.LoadInt32(&reservations) == 2 }, 3*time.Second, 20*time.Millisecond)
-	require.Less(t, counter, 100) // just make sure we're not busy-looping
+	require.Less(t, int(atomic.LoadInt32(&counter)), 100) // just make sure we're not busy-looping
 	require.Equal(t, 2, int(atomic.LoadInt32(&reservations)))
 }
 
@@ -295,7 +296,7 @@ func TestRelayV1(t *testing.T) {
 		close(peerChan)
 
 		h := newPrivateNode(t,
-			autorelay.WithPeerSource(func(int) <-chan peer.AddrInfo { return peerChan }, time.Hour),
+			autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo { return peerChan }, time.Hour),
 			autorelay.WithBootDelay(0),
 		)
 		defer h.Close()
@@ -311,7 +312,7 @@ func TestRelayV1(t *testing.T) {
 		close(peerChan)
 
 		h := newPrivateNode(t,
-			autorelay.WithPeerSource(func(int) <-chan peer.AddrInfo { return peerChan }, time.Hour),
+			autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo { return peerChan }, time.Hour),
 			autorelay.WithBootDelay(0),
 			autorelay.WithCircuitV1Support(),
 		)
@@ -332,7 +333,7 @@ func TestConnectOnDisconnect(t *testing.T) {
 		relays = append(relays, r)
 	}
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(int) <-chan peer.AddrInfo { return peerChan }, time.Hour),
+		autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo { return peerChan }, time.Hour),
 		autorelay.WithMinCandidates(1),
 		autorelay.WithMaxCandidates(num),
 		autorelay.WithNumRelays(1),
@@ -381,7 +382,7 @@ func TestMaxAge(t *testing.T) {
 	close(peerChans)
 
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(int) <-chan peer.AddrInfo {
+		autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo {
 			c, ok := <-peerChans
 			if !ok {
 				t.Fatal("unexpected call to PeerSource")
diff --git a/p2p/host/basic/basic_host_test.go b/p2p/host/basic/basic_host_test.go
index 02243c23..b1c2cf62 100644
--- a/p2p/host/basic/basic_host_test.go
+++ b/p2p/host/basic/basic_host_test.go
@@ -17,14 +17,12 @@ import (
 	"github.com/libp2p/go-libp2p/core/peerstore"
 	"github.com/libp2p/go-libp2p/core/protocol"
 	"github.com/libp2p/go-libp2p/core/record"
-	"github.com/libp2p/go-libp2p/core/test"
 	"github.com/libp2p/go-libp2p/p2p/host/autonat"
 	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
 	swarmt "github.com/libp2p/go-libp2p/p2p/net/swarm/testing"
 	"github.com/libp2p/go-libp2p/p2p/protocol/identify"
 
 	ma "github.com/multiformats/go-multiaddr"
-	madns "github.com/multiformats/go-multiaddr-dns"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -526,111 +524,6 @@ func TestProtoDowngrade(t *testing.T) {
 	assertWait(t, connectedOn, "/testing")
 }
 
-func TestAddrResolution(t *testing.T) {
-	ctx := context.Background()
-
-	p1 := test.RandPeerIDFatal(t)
-	p2 := test.RandPeerIDFatal(t)
-	addr1 := ma.StringCast("/dnsaddr/example.com")
-	addr2 := ma.StringCast("/ip4/192.0.2.1/tcp/123")
-	p2paddr1 := ma.StringCast("/dnsaddr/example.com/p2p/" + p1.Pretty())
-	p2paddr2 := ma.StringCast("/ip4/192.0.2.1/tcp/123/p2p/" + p1.Pretty())
-	p2paddr3 := ma.StringCast("/ip4/192.0.2.1/tcp/123/p2p/" + p2.Pretty())
-
-	backend := &madns.MockResolver{
-		TXT: map[string][]string{"_dnsaddr.example.com": {
-			"dnsaddr=" + p2paddr2.String(), "dnsaddr=" + p2paddr3.String(),
-		}},
-	}
-	resolver, err := madns.NewResolver(madns.WithDefaultResolver(backend))
-	require.NoError(t, err)
-
-	h, err := NewHost(swarmt.GenSwarm(t), &HostOpts{MultiaddrResolver: resolver})
-	require.NoError(t, err)
-	defer h.Close()
-
-	pi, err := peer.AddrInfoFromP2pAddr(p2paddr1)
-	require.NoError(t, err)
-
-	tctx, cancel := context.WithTimeout(ctx, time.Millisecond*100)
-	defer cancel()
-	_ = h.Connect(tctx, *pi)
-
-	addrs := h.Peerstore().Addrs(pi.ID)
-
-	require.Len(t, addrs, 2)
-	require.Contains(t, addrs, addr1)
-	require.Contains(t, addrs, addr2)
-}
-
-func TestAddrResolutionRecursive(t *testing.T) {
-	ctx := context.Background()
-
-	p1, err := test.RandPeerID()
-	if err != nil {
-		t.Error(err)
-	}
-	p2, err := test.RandPeerID()
-	if err != nil {
-		t.Error(err)
-	}
-	addr1 := ma.StringCast("/dnsaddr/example.com")
-	addr2 := ma.StringCast("/ip4/192.0.2.1/tcp/123")
-	p2paddr1 := ma.StringCast("/dnsaddr/example.com/p2p/" + p1.Pretty())
-	p2paddr2 := ma.StringCast("/dnsaddr/example.com/p2p/" + p2.Pretty())
-	p2paddr1i := ma.StringCast("/dnsaddr/foo.example.com/p2p/" + p1.Pretty())
-	p2paddr2i := ma.StringCast("/dnsaddr/bar.example.com/p2p/" + p2.Pretty())
-	p2paddr1f := ma.StringCast("/ip4/192.0.2.1/tcp/123/p2p/" + p1.Pretty())
-
-	backend := &madns.MockResolver{
-		TXT: map[string][]string{
-			"_dnsaddr.example.com": {
-				"dnsaddr=" + p2paddr1i.String(),
-				"dnsaddr=" + p2paddr2i.String(),
-			},
-			"_dnsaddr.foo.example.com": {
-				"dnsaddr=" + p2paddr1f.String(),
-			},
-			"_dnsaddr.bar.example.com": {
-				"dnsaddr=" + p2paddr2i.String(),
-			},
-		},
-	}
-	resolver, err := madns.NewResolver(madns.WithDefaultResolver(backend))
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	h, err := NewHost(swarmt.GenSwarm(t), &HostOpts{MultiaddrResolver: resolver})
-	require.NoError(t, err)
-	defer h.Close()
-
-	pi1, err := peer.AddrInfoFromP2pAddr(p2paddr1)
-	if err != nil {
-		t.Error(err)
-	}
-
-	tctx, cancel := context.WithTimeout(ctx, time.Millisecond*100)
-	defer cancel()
-	_ = h.Connect(tctx, *pi1)
-
-	addrs1 := h.Peerstore().Addrs(pi1.ID)
-	require.Len(t, addrs1, 2)
-	require.Contains(t, addrs1, addr1)
-	require.Contains(t, addrs1, addr2)
-
-	pi2, err := peer.AddrInfoFromP2pAddr(p2paddr2)
-	if err != nil {
-		t.Error(err)
-	}
-
-	_ = h.Connect(tctx, *pi2)
-
-	addrs2 := h.Peerstore().Addrs(pi2.ID)
-	require.Len(t, addrs2, 1)
-	require.Contains(t, addrs2, addr1)
-}
-
 func TestAddrChangeImmediatelyIfAddressNonEmpty(t *testing.T) {
 	ctx := context.Background()
 	taddrs := []ma.Multiaddr{ma.StringCast("/ip4/1.2.3.4/tcp/1234")}
diff --git a/p2p/muxer/testsuite/mux.go b/p2p/muxer/testsuite/mux.go
index d77d4073..5b47117f 100644
--- a/p2p/muxer/testsuite/mux.go
+++ b/p2p/muxer/testsuite/mux.go
@@ -71,8 +71,18 @@ func (p *peerScope) Check(t *testing.T) {
 	require.Zero(t, p.memory, "expected all reserved memory to have been released")
 }
 
+type peerScopeSpan struct {
+	peerScope
+}
+
+func (p *peerScopeSpan) Done() {
+	p.mx.Lock()
+	defer p.mx.Unlock()
+	p.memory = 0
+}
+
 func (p *peerScope) Stat() network.ScopeStat                       { return network.ScopeStat{} }
-func (p *peerScope) BeginSpan() (network.ResourceScopeSpan, error) { return nil, nil }
+func (p *peerScope) BeginSpan() (network.ResourceScopeSpan, error) { return &peerScopeSpan{}, nil }
 func (p *peerScope) Peer() peer.ID                                 { panic("implement me") }
 
 var _ network.PeerScope = &peerScope{}
diff --git a/p2p/net/swarm/dial_test.go b/p2p/net/swarm/dial_test.go
index aed15d4a..9b8c9e93 100644
--- a/p2p/net/swarm/dial_test.go
+++ b/p2p/net/swarm/dial_test.go
@@ -11,17 +11,18 @@ import (
 	"github.com/libp2p/go-libp2p/core/peer"
 	"github.com/libp2p/go-libp2p/core/peerstore"
 	testutil "github.com/libp2p/go-libp2p/core/test"
-	. "github.com/libp2p/go-libp2p/p2p/net/swarm"
+	"github.com/libp2p/go-libp2p/p2p/net/swarm"
 	swarmt "github.com/libp2p/go-libp2p/p2p/net/swarm/testing"
 
 	"github.com/libp2p/go-libp2p-testing/ci"
 
 	ma "github.com/multiformats/go-multiaddr"
+	madns "github.com/multiformats/go-multiaddr-dns"
 	manet "github.com/multiformats/go-multiaddr/net"
 	"github.com/stretchr/testify/require"
 )
 
-func closeSwarms(swarms []*Swarm) {
+func closeSwarms(swarms []*swarm.Swarm) {
 	for _, s := range swarms {
 		s.Close()
 	}
@@ -45,6 +46,44 @@ func TestBasicDialPeer(t *testing.T) {
 	s.Close()
 }
 
+func TestBasicDialPeerWithResolver(t *testing.T) {
+	t.Parallel()
+
+	mockResolver := madns.MockResolver{IP: make(map[string][]net.IPAddr)}
+	ipaddr, err := net.ResolveIPAddr("ip4", "127.0.0.1")
+	require.NoError(t, err)
+	mockResolver.IP["example.com"] = []net.IPAddr{*ipaddr}
+	resolver, err := madns.NewResolver(madns.WithDomainResolver("example.com", &mockResolver))
+	require.NoError(t, err)
+
+	swarms := makeSwarms(t, 2, swarmt.WithSwarmOpts(swarm.WithMultiaddrResolver(resolver)))
+	defer closeSwarms(swarms)
+	s1 := swarms[0]
+	s2 := swarms[1]
+
+	// Change the multiaddr from /ip4/127.0.0.1/... to /dns4/example.com/... so
+	// that the resovler has to resolve this
+	var s2Addrs []ma.Multiaddr
+	for _, a := range s2.ListenAddresses() {
+		_, rest := ma.SplitFunc(a, func(c ma.Component) bool {
+			return c.Protocol().Code == ma.P_TCP || c.Protocol().Code == ma.P_UDP
+		},
+		)
+		if rest != nil {
+			s2Addrs = append(s2Addrs, ma.StringCast("/dns4/example.com").Encapsulate(rest))
+		}
+	}
+
+	s1.Peerstore().AddAddrs(s2.LocalPeer(), s2Addrs, peerstore.PermanentAddrTTL)
+
+	c, err := s1.DialPeer(context.Background(), s2.LocalPeer())
+	require.NoError(t, err)
+
+	s, err := c.NewStream(context.Background())
+	require.NoError(t, err)
+	s.Close()
+}
+
 func TestDialWithNoListeners(t *testing.T) {
 	t.Parallel()
 
@@ -90,7 +129,7 @@ func TestSimultDials(t *testing.T) {
 	{
 		var wg sync.WaitGroup
 		errs := make(chan error, 20) // 2 connect calls in each of the 10 for-loop iterations
-		connect := func(s *Swarm, dst peer.ID, addr ma.Multiaddr) {
+		connect := func(s *swarm.Swarm, dst peer.ID, addr ma.Multiaddr) {
 			// copy for other peer
 			log.Debugf("TestSimultOpen: connecting: %s --> %s (%s)", s.LocalPeer(), dst, addr)
 			s.Peerstore().AddAddr(dst, addr, peerstore.TempAddrTTL)
@@ -157,7 +196,7 @@ func newSilentPeer(t *testing.T) (peer.ID, ma.Multiaddr, net.Listener) {
 func TestDialWait(t *testing.T) {
 	const dialTimeout = 250 * time.Millisecond
 
-	swarms := makeSwarms(t, 1, swarmt.DialTimeout(dialTimeout))
+	swarms := makeSwarms(t, 1, swarmt.WithSwarmOpts(swarm.WithDialTimeout(dialTimeout)))
 	s1 := swarms[0]
 	defer s1.Close()
 
@@ -176,11 +215,11 @@ func TestDialWait(t *testing.T) {
 	}
 	duration := time.Since(before)
 
-	if duration < dialTimeout*DialAttempts {
-		t.Error("< dialTimeout * DialAttempts not being respected", duration, dialTimeout*DialAttempts)
+	if duration < dialTimeout*swarm.DialAttempts {
+		t.Error("< dialTimeout * DialAttempts not being respected", duration, dialTimeout*swarm.DialAttempts)
 	}
-	if duration > 2*dialTimeout*DialAttempts {
-		t.Error("> 2*dialTimeout * DialAttempts not being respected", duration, 2*dialTimeout*DialAttempts)
+	if duration > 2*dialTimeout*swarm.DialAttempts {
+		t.Error("> 2*dialTimeout * DialAttempts not being respected", duration, 2*dialTimeout*swarm.DialAttempts)
 	}
 
 	if !s1.Backoff().Backoff(s2p, s2addr) {
@@ -197,7 +236,7 @@ func TestDialBackoff(t *testing.T) {
 	const dialTimeout = 100 * time.Millisecond
 
 	ctx := context.Background()
-	swarms := makeSwarms(t, 2, swarmt.DialTimeout(dialTimeout))
+	swarms := makeSwarms(t, 2, swarmt.WithSwarmOpts(swarm.WithDialTimeout(dialTimeout)))
 	defer closeSwarms(swarms)
 	s1 := swarms[0]
 	s2 := swarms[1]
@@ -400,7 +439,7 @@ func TestDialBackoffClears(t *testing.T) {
 	t.Parallel()
 
 	const dialTimeout = 250 * time.Millisecond
-	swarms := makeSwarms(t, 2, swarmt.DialTimeout(dialTimeout))
+	swarms := makeSwarms(t, 2, swarmt.WithSwarmOpts(swarm.WithDialTimeout(dialTimeout)))
 	defer closeSwarms(swarms)
 	s1 := swarms[0]
 	s2 := swarms[1]
@@ -418,11 +457,11 @@ func TestDialBackoffClears(t *testing.T) {
 	require.Error(t, err, "dialing to broken addr worked...")
 	duration := time.Since(before)
 
-	if duration < dialTimeout*DialAttempts {
-		t.Error("< dialTimeout * DialAttempts not being respected", duration, dialTimeout*DialAttempts)
+	if duration < dialTimeout*swarm.DialAttempts {
+		t.Error("< dialTimeout * DialAttempts not being respected", duration, dialTimeout*swarm.DialAttempts)
 	}
-	if duration > 2*dialTimeout*DialAttempts {
-		t.Error("> 2*dialTimeout * DialAttempts not being respected", duration, 2*dialTimeout*DialAttempts)
+	if duration > 2*dialTimeout*swarm.DialAttempts {
+		t.Error("> 2*dialTimeout * DialAttempts not being respected", duration, 2*dialTimeout*swarm.DialAttempts)
 	}
 	require.True(t, s1.Backoff().Backoff(s2.LocalPeer(), s2bad), "s2 should now be on backoff")
 
@@ -441,7 +480,7 @@ func TestDialBackoffClears(t *testing.T) {
 func TestDialPeerFailed(t *testing.T) {
 	t.Parallel()
 
-	swarms := makeSwarms(t, 2, swarmt.DialTimeout(100*time.Millisecond))
+	swarms := makeSwarms(t, 2, swarmt.WithSwarmOpts(swarm.WithDialTimeout(100*time.Millisecond)))
 	defer closeSwarms(swarms)
 	testedSwarm, targetSwarm := swarms[0], swarms[1]
 
@@ -462,7 +501,7 @@ func TestDialPeerFailed(t *testing.T) {
 	//   * [/ip4/127.0.0.1/tcp/34881] failed to negotiate security protocol: context deadline exceeded
 	// ...
 
-	dialErr, ok := err.(*DialError)
+	dialErr, ok := err.(*swarm.DialError)
 	if !ok {
 		t.Fatalf("expected *DialError, got %T", err)
 	}
@@ -515,7 +554,7 @@ func newSilentListener(t *testing.T) ([]ma.Multiaddr, net.Listener) {
 func TestDialSimultaneousJoin(t *testing.T) {
 	const dialTimeout = 250 * time.Millisecond
 
-	swarms := makeSwarms(t, 2, swarmt.DialTimeout(dialTimeout))
+	swarms := makeSwarms(t, 2, swarmt.WithSwarmOpts(swarm.WithDialTimeout(dialTimeout)))
 	defer closeSwarms(swarms)
 	s1 := swarms[0]
 	s2 := swarms[1]
@@ -617,5 +656,5 @@ func TestDialSelf(t *testing.T) {
 	s1 := swarms[0]
 
 	_, err := s1.DialPeer(context.Background(), s1.LocalPeer())
-	require.ErrorIs(t, err, ErrDialToSelf, "expected error from self dial")
+	require.ErrorIs(t, err, swarm.ErrDialToSelf, "expected error from self dial")
 }
diff --git a/p2p/net/swarm/swarm_dial_test.go b/p2p/net/swarm/swarm_dial_test.go
new file mode 100644
index 00000000..89794336
--- /dev/null
+++ b/p2p/net/swarm/swarm_dial_test.go
@@ -0,0 +1,193 @@
+package swarm
+
+import (
+	"context"
+	"crypto/rand"
+	"net"
+	"testing"
+	"time"
+
+	"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/peerstore"
+	"github.com/libp2p/go-libp2p/core/test"
+	"github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem"
+	"github.com/libp2p/go-libp2p/p2p/transport/tcp"
+	"github.com/libp2p/go-libp2p/p2p/transport/websocket"
+	"github.com/multiformats/go-multiaddr"
+	madns "github.com/multiformats/go-multiaddr-dns"
+	"github.com/stretchr/testify/require"
+)
+
+func TestAddrsForDial(t *testing.T) {
+	mockResolver := madns.MockResolver{IP: make(map[string][]net.IPAddr)}
+	ipaddr, err := net.ResolveIPAddr("ip4", "1.2.3.4")
+	if err != nil {
+		t.Fatal(err)
+	}
+	mockResolver.IP["example.com"] = []net.IPAddr{*ipaddr}
+
+	resolver, err := madns.NewResolver(madns.WithDomainResolver("example.com", &mockResolver))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	priv, _, err := crypto.GenerateEd25519Key(rand.Reader)
+	require.NoError(t, err)
+	id, err := peer.IDFromPrivateKey(priv)
+	require.NoError(t, err)
+
+	ps, err := pstoremem.NewPeerstore()
+	require.NoError(t, err)
+	ps.AddPubKey(id, priv.GetPublic())
+	ps.AddPrivKey(id, priv)
+	t.Cleanup(func() { ps.Close() })
+
+	tpt, err := websocket.New(nil, network.NullResourceManager)
+	require.NoError(t, err)
+	s, err := NewSwarm(id, ps, WithMultiaddrResolver(resolver))
+	require.NoError(t, err)
+	defer s.Close()
+	err = s.AddTransport(tpt)
+	require.NoError(t, err)
+
+	otherPeer := test.RandPeerIDFatal(t)
+
+	ps.AddAddr(otherPeer, multiaddr.StringCast("/dns4/example.com/tcp/1234/wss"), time.Hour)
+
+	ctx := context.Background()
+	mas, err := s.addrsForDial(ctx, otherPeer)
+	require.NoError(t, err)
+
+	require.NotZero(t, len(mas))
+}
+
+func newTestSwarmWithResolver(t *testing.T, resolver *madns.Resolver) *Swarm {
+	priv, _, err := crypto.GenerateEd25519Key(rand.Reader)
+	require.NoError(t, err)
+	id, err := peer.IDFromPrivateKey(priv)
+	require.NoError(t, err)
+	ps, err := pstoremem.NewPeerstore()
+	require.NoError(t, err)
+	ps.AddPubKey(id, priv.GetPublic())
+	ps.AddPrivKey(id, priv)
+	t.Cleanup(func() { ps.Close() })
+	s, err := NewSwarm(id, ps, WithMultiaddrResolver(resolver))
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		s.Close()
+	})
+
+	// Add a tcp transport so that we know we can dial a tcp multiaddr and we don't filter it out.
+	tpt, err := tcp.NewTCPTransport(nil, network.NullResourceManager)
+	require.NoError(t, err)
+	err = s.AddTransport(tpt)
+	require.NoError(t, err)
+
+	return s
+}
+
+func TestAddrResolution(t *testing.T) {
+	ctx := context.Background()
+
+	p1 := test.RandPeerIDFatal(t)
+	p2 := test.RandPeerIDFatal(t)
+	addr1 := multiaddr.StringCast("/dnsaddr/example.com")
+	addr2 := multiaddr.StringCast("/ip4/192.0.2.1/tcp/123")
+
+	p2paddr2 := multiaddr.StringCast("/ip4/192.0.2.1/tcp/123/p2p/" + p1.Pretty())
+	p2paddr3 := multiaddr.StringCast("/ip4/192.0.2.1/tcp/123/p2p/" + p2.Pretty())
+
+	backend := &madns.MockResolver{
+		TXT: map[string][]string{"_dnsaddr.example.com": {
+			"dnsaddr=" + p2paddr2.String(), "dnsaddr=" + p2paddr3.String(),
+		}},
+	}
+	resolver, err := madns.NewResolver(madns.WithDefaultResolver(backend))
+	require.NoError(t, err)
+
+	s := newTestSwarmWithResolver(t, resolver)
+
+	s.peers.AddAddr(p1, addr1, time.Hour)
+
+	tctx, cancel := context.WithTimeout(ctx, time.Millisecond*100)
+	defer cancel()
+	mas, err := s.addrsForDial(tctx, p1)
+	require.NoError(t, err)
+
+	require.Len(t, mas, 1)
+	require.Contains(t, mas, addr2)
+
+	addrs := s.peers.Addrs(p1)
+	require.Len(t, addrs, 2)
+	require.Contains(t, addrs, addr1)
+	require.Contains(t, addrs, addr2)
+}
+
+func TestAddrResolutionRecursive(t *testing.T) {
+	ctx := context.Background()
+
+	p1, err := test.RandPeerID()
+	if err != nil {
+		t.Error(err)
+	}
+	p2, err := test.RandPeerID()
+	if err != nil {
+		t.Error(err)
+	}
+	addr1 := multiaddr.StringCast("/dnsaddr/example.com")
+	addr2 := multiaddr.StringCast("/ip4/192.0.2.1/tcp/123")
+	p2paddr1 := multiaddr.StringCast("/dnsaddr/example.com/p2p/" + p1.Pretty())
+	p2paddr2 := multiaddr.StringCast("/dnsaddr/example.com/p2p/" + p2.Pretty())
+	p2paddr1i := multiaddr.StringCast("/dnsaddr/foo.example.com/p2p/" + p1.Pretty())
+	p2paddr2i := multiaddr.StringCast("/dnsaddr/bar.example.com/p2p/" + p2.Pretty())
+	p2paddr1f := multiaddr.StringCast("/ip4/192.0.2.1/tcp/123/p2p/" + p1.Pretty())
+
+	backend := &madns.MockResolver{
+		TXT: map[string][]string{
+			"_dnsaddr.example.com": {
+				"dnsaddr=" + p2paddr1i.String(),
+				"dnsaddr=" + p2paddr2i.String(),
+			},
+			"_dnsaddr.foo.example.com": {
+				"dnsaddr=" + p2paddr1f.String(),
+			},
+			"_dnsaddr.bar.example.com": {
+				"dnsaddr=" + p2paddr2i.String(),
+			},
+		},
+	}
+	resolver, err := madns.NewResolver(madns.WithDefaultResolver(backend))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	s := newTestSwarmWithResolver(t, resolver)
+
+	pi1, err := peer.AddrInfoFromP2pAddr(p2paddr1)
+	require.NoError(t, err)
+
+	tctx, cancel := context.WithTimeout(ctx, time.Millisecond*100)
+	defer cancel()
+	s.Peerstore().AddAddrs(pi1.ID, pi1.Addrs, peerstore.TempAddrTTL)
+	_, err = s.addrsForDial(tctx, p1)
+	require.NoError(t, err)
+
+	addrs1 := s.Peerstore().Addrs(pi1.ID)
+	require.Len(t, addrs1, 2)
+	require.Contains(t, addrs1, addr1)
+	require.Contains(t, addrs1, addr2)
+
+	pi2, err := peer.AddrInfoFromP2pAddr(p2paddr2)
+	require.NoError(t, err)
+
+	s.Peerstore().AddAddrs(pi2.ID, pi2.Addrs, peerstore.TempAddrTTL)
+	_, err = s.addrsForDial(tctx, p2)
+	// This never resolves to a good address
+	require.Equal(t, ErrNoGoodAddresses, err)
+
+	addrs2 := s.Peerstore().Addrs(pi2.ID)
+	require.Len(t, addrs2, 1)
+	require.Contains(t, addrs2, addr1)
+}
diff --git a/p2p/net/swarm/swarm_test.go b/p2p/net/swarm/swarm_test.go
index f25ee1f1..95aa71d7 100644
--- a/p2p/net/swarm/swarm_test.go
+++ b/p2p/net/swarm/swarm_test.go
@@ -17,6 +17,7 @@ import (
 	"github.com/libp2p/go-libp2p/core/peer"
 	"github.com/libp2p/go-libp2p/core/peerstore"
 	"github.com/libp2p/go-libp2p/core/protocol"
+	"github.com/libp2p/go-libp2p/core/test"
 	"github.com/libp2p/go-libp2p/p2p/net/swarm"
 	. "github.com/libp2p/go-libp2p/p2p/net/swarm/testing"
 
@@ -407,7 +408,7 @@ func TestPreventDialListenAddr(t *testing.T) {
 			break
 		}
 	}
-	remote := peer.ID("foobar")
+	remote := test.RandPeerIDFatal(t)
 	s.Peerstore().AddAddr(remote, addr, time.Hour)
 	_, err = s.DialPeer(context.Background(), remote)
 	if !errors.Is(err, swarm.ErrNoGoodAddresses) {
@@ -461,11 +462,11 @@ func TestResourceManager(t *testing.T) {
 	defer ctrl.Finish()
 
 	rcmgr1 := mocknetwork.NewMockResourceManager(ctrl)
-	s1 := GenSwarm(t, OptResourceManager(rcmgr1))
+	s1 := GenSwarm(t, WithSwarmOpts(swarm.WithResourceManager(rcmgr1)))
 	defer s1.Close()
 
 	rcmgr2 := mocknetwork.NewMockResourceManager(ctrl)
-	s2 := GenSwarm(t, OptResourceManager(rcmgr2))
+	s2 := GenSwarm(t, WithSwarmOpts(swarm.WithResourceManager(rcmgr2)))
 	defer s2.Close()
 	connectSwarms(t, context.Background(), []*swarm.Swarm{s1, s2})
 
@@ -496,7 +497,7 @@ func TestResourceManagerNewStream(t *testing.T) {
 	defer ctrl.Finish()
 
 	rcmgr1 := mocknetwork.NewMockResourceManager(ctrl)
-	s1 := GenSwarm(t, OptResourceManager(rcmgr1))
+	s1 := GenSwarm(t, WithSwarmOpts(swarm.WithResourceManager(rcmgr1)))
 	defer s1.Close()
 
 	s2 := GenSwarm(t)
@@ -515,11 +516,11 @@ func TestResourceManagerAcceptStream(t *testing.T) {
 	defer ctrl.Finish()
 
 	rcmgr1 := mocknetwork.NewMockResourceManager(ctrl)
-	s1 := GenSwarm(t, OptResourceManager(rcmgr1))
+	s1 := GenSwarm(t, WithSwarmOpts(swarm.WithResourceManager(rcmgr1)))
 	defer s1.Close()
 
 	rcmgr2 := mocknetwork.NewMockResourceManager(ctrl)
-	s2 := GenSwarm(t, OptResourceManager(rcmgr2))
+	s2 := GenSwarm(t, WithSwarmOpts(swarm.WithResourceManager(rcmgr2)))
 	defer s2.Close()
 	s2.SetStreamHandler(func(str network.Stream) { t.Fatal("didn't expect to accept a stream") })
 
diff --git a/p2p/net/swarm/testing/testing.go b/p2p/net/swarm/testing/testing.go
index c3704e5d..604d1d0a 100644
--- a/p2p/net/swarm/testing/testing.go
+++ b/p2p/net/swarm/testing/testing.go
@@ -32,10 +32,9 @@ type config struct {
 	dialOnly         bool
 	disableTCP       bool
 	disableQUIC      bool
-	dialTimeout      time.Duration
 	connectionGater  connmgr.ConnectionGater
-	rcmgr            network.ResourceManager
 	sk               crypto.PrivKey
+	swarmOpts        []swarm.Option
 	clock
 }
 
@@ -59,6 +58,12 @@ func WithClock(clock clock) Option {
 	}
 }
 
+func WithSwarmOpts(swarmOpts ...swarm.Option) Option {
+	return func(_ *testing.T, c *config) {
+		c.swarmOpts = swarmOpts
+	}
+}
+
 // OptDisableReuseport disables reuseport in this test swarm.
 var OptDisableReuseport Option = func(_ *testing.T, c *config) {
 	c.disableReuseport = true
@@ -86,12 +91,6 @@ func OptConnGater(cg connmgr.ConnectionGater) Option {
 	}
 }
 
-func OptResourceManager(rcmgr network.ResourceManager) Option {
-	return func(_ *testing.T, c *config) {
-		c.rcmgr = rcmgr
-	}
-}
-
 // OptPeerPrivateKey configures the peer private key which is then used to derive the public key and peer ID.
 func OptPeerPrivateKey(sk crypto.PrivKey) Option {
 	return func(_ *testing.T, c *config) {
@@ -99,12 +98,6 @@ func OptPeerPrivateKey(sk crypto.PrivKey) Option {
 	}
 }
 
-func DialTimeout(t time.Duration) Option {
-	return func(_ *testing.T, c *config) {
-		c.dialTimeout = t
-	}
-}
-
 // GenUpgrader creates a new connection upgrader for use with this swarm.
 func GenUpgrader(t *testing.T, n *swarm.Swarm, opts ...tptu.Option) transport.Upgrader {
 	id := n.LocalPeer()
@@ -144,16 +137,11 @@ func GenSwarm(t *testing.T, opts ...Option) *swarm.Swarm {
 	ps.AddPrivKey(id, priv)
 	t.Cleanup(func() { ps.Close() })
 
-	swarmOpts := []swarm.Option{swarm.WithMetrics(metrics.NewBandwidthCounter())}
+	swarmOpts := cfg.swarmOpts
+	swarmOpts = append(swarmOpts, swarm.WithMetrics(metrics.NewBandwidthCounter()))
 	if cfg.connectionGater != nil {
 		swarmOpts = append(swarmOpts, swarm.WithConnectionGater(cfg.connectionGater))
 	}
-	if cfg.rcmgr != nil {
-		swarmOpts = append(swarmOpts, swarm.WithResourceManager(cfg.rcmgr))
-	}
-	if cfg.dialTimeout != 0 {
-		swarmOpts = append(swarmOpts, swarm.WithDialTimeout(cfg.dialTimeout))
-	}
 	s, err := swarm.NewSwarm(id, ps, swarmOpts...)
 	require.NoError(t, err)
 
diff --git a/p2p/security/noise/crypto_test.go b/p2p/security/noise/crypto_test.go
index 35837a5a..9b7d3908 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, true)
+	session, _ := newSecureSession(initTransport, context.TODO(), init, "remote-peer", nil, nil, 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 5269be29..f86705f4 100644
--- a/p2p/security/noise/transport_test.go
+++ b/p2p/security/noise/transport_test.go
@@ -78,10 +78,10 @@ func connect(t *testing.T, initTransport, respTransport *Transport) (*secureSess
 	done := make(chan struct{})
 	go func() {
 		defer close(done)
-		initConn, initErr = initTransport.SecureOutbound(context.TODO(), init, respTransport.localID)
+		initConn, initErr = initTransport.SecureOutbound(context.Background(), init, respTransport.localID)
 	}()
 
-	respConn, respErr := respTransport.SecureInbound(context.TODO(), resp, "")
+	respConn, respErr := respTransport.SecureInbound(context.Background(), resp, "")
 	<-done
 
 	if initErr != nil {
@@ -171,7 +171,7 @@ func TestPeerIDMatch(t *testing.T) {
 	done := make(chan struct{})
 	go func() {
 		defer close(done)
-		conn, err := initTransport.SecureOutbound(context.TODO(), init, respTransport.localID)
+		conn, err := initTransport.SecureOutbound(context.Background(), init, respTransport.localID)
 		assert.NoError(t, err)
 		assert.Equal(t, conn.RemotePeer(), respTransport.localID)
 		b := make([]byte, 6)
@@ -180,7 +180,7 @@ func TestPeerIDMatch(t *testing.T) {
 		assert.Equal(t, b, []byte("foobar"))
 	}()
 
-	conn, err := respTransport.SecureInbound(context.TODO(), resp, initTransport.localID)
+	conn, err := respTransport.SecureInbound(context.Background(), resp, initTransport.localID)
 	require.NoError(t, err)
 	require.Equal(t, conn.RemotePeer(), initTransport.localID)
 	_, err = conn.Write([]byte("foobar"))
@@ -194,11 +194,11 @@ func TestPeerIDMismatchOutboundFailsHandshake(t *testing.T) {
 
 	errChan := make(chan error)
 	go func() {
-		_, err := initTransport.SecureOutbound(context.TODO(), init, "a-random-peer-id")
+		_, err := initTransport.SecureOutbound(context.Background(), init, "a-random-peer-id")
 		errChan <- err
 	}()
 
-	_, err := respTransport.SecureInbound(context.TODO(), resp, "")
+	_, err := respTransport.SecureInbound(context.Background(), resp, "")
 	require.Error(t, err)
 
 	initErr := <-errChan
@@ -214,13 +214,13 @@ func TestPeerIDMismatchInboundFailsHandshake(t *testing.T) {
 	done := make(chan struct{})
 	go func() {
 		defer close(done)
-		conn, err := initTransport.SecureOutbound(context.TODO(), init, respTransport.localID)
+		conn, err := initTransport.SecureOutbound(context.Background(), init, respTransport.localID)
 		assert.NoError(t, err)
 		_, err = conn.Read([]byte{0})
 		assert.Error(t, err)
 	}()
 
-	_, err := respTransport.SecureInbound(context.TODO(), resp, "a-random-peer-id")
+	_, err := respTransport.SecureInbound(context.Background(), resp, "a-random-peer-id")
 	require.Error(t, err, "expected responder to fail with peer ID mismatch error")
 	<-done
 }
@@ -387,7 +387,7 @@ func TestPrologueMatches(t *testing.T) {
 		tpt, err := initTransport.
 			WithSessionOptions(Prologue(commonPrologue))
 		require.NoError(t, err)
-		conn, err := tpt.SecureOutbound(context.TODO(), initConn, respTransport.localID)
+		conn, err := tpt.SecureOutbound(context.Background(), initConn, respTransport.localID)
 		require.NoError(t, err)
 		defer conn.Close()
 	}()
@@ -395,7 +395,7 @@ func TestPrologueMatches(t *testing.T) {
 	tpt, err := respTransport.
 		WithSessionOptions(Prologue(commonPrologue))
 	require.NoError(t, err)
-	conn, err := tpt.SecureInbound(context.TODO(), respConn, "")
+	conn, err := tpt.SecureInbound(context.Background(), respConn, "")
 	require.NoError(t, err)
 	defer conn.Close()
 	<-done
@@ -415,14 +415,125 @@ func TestPrologueDoesNotMatchFailsHandshake(t *testing.T) {
 		tpt, err := initTransport.
 			WithSessionOptions(Prologue(initPrologue))
 		require.NoError(t, err)
-		_, err = tpt.SecureOutbound(context.TODO(), initConn, respTransport.localID)
+		_, err = tpt.SecureOutbound(context.Background(), initConn, respTransport.localID)
 		require.Error(t, err)
 	}()
 
 	tpt, err := respTransport.WithSessionOptions(Prologue(respPrologue))
 	require.NoError(t, err)
 
-	_, err = tpt.SecureInbound(context.TODO(), respConn, "")
+	_, err = tpt.SecureInbound(context.Background(), respConn, "")
 	require.Error(t, err)
 	<-done
 }
+
+type earlyDataHandler struct {
+	send     func(context.Context, net.Conn, peer.ID) []byte
+	received func(context.Context, net.Conn, []byte) error
+}
+
+func (e *earlyDataHandler) Send(ctx context.Context, conn net.Conn, id peer.ID) []byte {
+	if e.send == nil {
+		return nil
+	}
+	return e.send(ctx, conn, id)
+}
+
+func (e *earlyDataHandler) Received(ctx context.Context, conn net.Conn, data []byte) error {
+	if e.received == nil {
+		return nil
+	}
+	return e.received(ctx, conn, data)
+}
+
+func TestEarlyDataAccepted(t *testing.T) {
+	var receivedEarlyData []byte
+	serverEDH := &earlyDataHandler{
+		received: func(_ context.Context, _ net.Conn, data []byte) error {
+			receivedEarlyData = data
+			return nil
+		},
+	}
+	clientEDH := &earlyDataHandler{
+		send: func(ctx context.Context, conn net.Conn, id peer.ID) []byte { return []byte("foobar") },
+	}
+	initTransport, err := newTestTransport(t, crypto.Ed25519, 2048).WithSessionOptions(EarlyData(clientEDH))
+	require.NoError(t, err)
+	tpt := newTestTransport(t, crypto.Ed25519, 2048)
+	respTransport, err := tpt.WithSessionOptions(EarlyData(serverEDH))
+	require.NoError(t, err)
+
+	initConn, respConn := newConnPair(t)
+
+	errChan := make(chan error)
+	go func() {
+		_, err := respTransport.SecureInbound(context.Background(), initConn, "")
+		errChan <- err
+	}()
+
+	conn, err := initTransport.SecureOutbound(context.Background(), respConn, tpt.localID)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	require.Equal(t, []byte("foobar"), receivedEarlyData)
+}
+
+func TestEarlyDataRejected(t *testing.T) {
+	serverEDH := &earlyDataHandler{
+		received: func(_ context.Context, _ net.Conn, data []byte) error { return errors.New("nope") },
+	}
+	clientEDH := &earlyDataHandler{
+		send: func(ctx context.Context, conn net.Conn, id peer.ID) []byte { return []byte("foobar") },
+	}
+	initTransport, err := newTestTransport(t, crypto.Ed25519, 2048).WithSessionOptions(EarlyData(clientEDH))
+	require.NoError(t, err)
+	tpt := newTestTransport(t, crypto.Ed25519, 2048)
+	respTransport, err := tpt.WithSessionOptions(EarlyData(serverEDH))
+	require.NoError(t, err)
+
+	initConn, respConn := newConnPair(t)
+
+	errChan := make(chan error)
+	go func() {
+		_, err := respTransport.SecureInbound(context.Background(), initConn, "")
+		errChan <- err
+	}()
+
+	_, err = initTransport.SecureOutbound(context.Background(), respConn, tpt.localID)
+	require.Error(t, err)
+
+	select {
+	case <-time.After(500 * time.Millisecond):
+		t.Fatal("timeout")
+	case err := <-errChan:
+		require.EqualError(t, err, "nope")
+	}
+}
+
+func TestEarlyDataAcceptedWithNoHandler(t *testing.T) {
+	clientEDH := &earlyDataHandler{
+		send: func(ctx context.Context, conn net.Conn, id peer.ID) []byte { return []byte("foobar") },
+	}
+	initTransport, err := newTestTransport(t, crypto.Ed25519, 2048).WithSessionOptions(EarlyData(clientEDH))
+	require.NoError(t, err)
+	respTransport := newTestTransport(t, crypto.Ed25519, 2048)
+
+	initConn, respConn := newConnPair(t)
+
+	errChan := make(chan error)
+	go func() {
+		_, err := respTransport.SecureInbound(context.Background(), initConn, "")
+		errChan <- err
+	}()
+
+	conn, err := initTransport.SecureOutbound(context.Background(), respConn, respTransport.localID)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	select {
+	case <-time.After(500 * time.Millisecond):
+		t.Fatal("timeout")
+	case err := <-errChan:
+		require.NoError(t, err)
+	}
+}
diff --git a/p2p/transport/tcp/tcp_test.go b/p2p/transport/tcp/tcp_test.go
index f73e511a..f44c16c8 100644
--- a/p2p/transport/tcp/tcp_test.go
+++ b/p2p/transport/tcp/tcp_test.go
@@ -44,6 +44,23 @@ func TestTcpTransport(t *testing.T) {
 	envReuseportVal = true
 }
 
+func TestTcpTransportWithMetrics(t *testing.T) {
+	peerA, ia := makeInsecureMuxer(t)
+	_, ib := makeInsecureMuxer(t)
+
+	ua, err := tptu.New(ia, yamux.DefaultTransport)
+	require.NoError(t, err)
+	ta, err := NewTCPTransport(ua, nil, WithMetrics())
+	require.NoError(t, err)
+	ub, err := tptu.New(ib, yamux.DefaultTransport)
+	require.NoError(t, err)
+	tb, err := NewTCPTransport(ub, nil, WithMetrics())
+	require.NoError(t, err)
+
+	zero := "/ip4/127.0.0.1/tcp/0"
+	ttransport.SubtestTransport(t, ta, tb, zero, peerA)
+}
+
 func TestResourceManager(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
diff --git a/p2p/transport/websocket/websocket_test.go b/p2p/transport/websocket/websocket_test.go
index 77dda801..9ac71a77 100644
--- a/p2p/transport/websocket/websocket_test.go
+++ b/p2p/transport/websocket/websocket_test.go
@@ -2,6 +2,8 @@ package websocket
 
 import (
 	"context"
+	"crypto/ecdsa"
+	"crypto/elliptic"
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/tls"
@@ -24,6 +26,7 @@ import (
 	"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"
+	"github.com/libp2p/go-libp2p/p2p/security/noise"
 	ttransport "github.com/libp2p/go-libp2p/p2p/transport/testsuite"
 
 	ma "github.com/multiformats/go-multiaddr"
@@ -31,6 +34,16 @@ import (
 )
 
 func newUpgrader(t *testing.T) (peer.ID, transport.Upgrader) {
+	t.Helper()
+	id, m := newInsecureMuxer(t)
+	u, err := tptu.New(m, yamux.DefaultTransport)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return id, u
+}
+
+func newSecureUpgrader(t *testing.T) (peer.ID, transport.Upgrader) {
 	t.Helper()
 	id, m := newSecureMuxer(t)
 	u, err := tptu.New(m, yamux.DefaultTransport)
@@ -40,7 +53,7 @@ func newUpgrader(t *testing.T) (peer.ID, transport.Upgrader) {
 	return id, u
 }
 
-func newSecureMuxer(t *testing.T) (peer.ID, sec.SecureMuxer) {
+func newInsecureMuxer(t *testing.T) (peer.ID, sec.SecureMuxer) {
 	t.Helper()
 	priv, _, err := test.RandTestKeyPair(crypto.Ed25519, 256)
 	if err != nil {
@@ -55,15 +68,32 @@ func newSecureMuxer(t *testing.T) (peer.ID, sec.SecureMuxer) {
 	return id, &secMuxer
 }
 
+func newSecureMuxer(t *testing.T) (peer.ID, sec.SecureMuxer) {
+	t.Helper()
+	priv, _, err := test.RandTestKeyPair(crypto.Ed25519, 256)
+	if err != nil {
+		t.Fatal(err)
+	}
+	id, err := peer.IDFromPrivateKey(priv)
+	if err != nil {
+		t.Fatal(err)
+	}
+	var secMuxer csms.SSMuxer
+	noiseTpt, err := noise.New(priv)
+	require.NoError(t, err)
+	secMuxer.AddTransport(noise.ID, noiseTpt)
+	return id, &secMuxer
+}
+
 func lastComponent(t *testing.T, a ma.Multiaddr) ma.Multiaddr {
 	t.Helper()
 	_, wscomponent := ma.SplitLast(a)
 	require.NotNil(t, wscomponent)
-	if wscomponent.Equal(wsma) {
-		return wsma
+	if wscomponent.Equal(wsComponent) {
+		return wsComponent
 	}
-	if wscomponent.Equal(wssma) {
-		return wssma
+	if wscomponent.Equal(wssComponent) {
+		return wssComponent
 	}
 	t.Fatal("expected a ws or wss component")
 	return nil
@@ -102,33 +132,117 @@ func TestCanDial(t *testing.T) {
 	if d.CanDial(ma.StringCast("/ip4/127.0.0.1/tcp/5555")) {
 		t.Fatal("expected to not match tcp maddr, but did")
 	}
+	if !d.CanDial(ma.StringCast("/ip4/127.0.0.1/tcp/5555/tls/ws")) {
+		t.Fatal("expected to match secure websocket maddr, but did not")
+	}
+	if !d.CanDial(ma.StringCast("/ip4/127.0.0.1/tcp/5555/tls/sni/example.com/ws")) {
+		t.Fatal("expected to match secure websocket maddr with sni, but did not")
+	}
+	if !d.CanDial(ma.StringCast("/dns4/example.com/tcp/5555/tls/sni/example.com/ws")) {
+		t.Fatal("expected to match secure websocket maddr with sni, but did not")
+	}
+	if !d.CanDial(ma.StringCast("/dnsaddr/example.com/tcp/5555/tls/sni/example.com/ws")) {
+		t.Fatal("expected to match secure websocket maddr with sni, but did not")
+	}
 }
 
-func TestDialWss(t *testing.T) {
-	if _, err := net.LookupIP("nyc-1.bootstrap.libp2p.io"); err != nil {
-		t.Skip("this test requries an internet connection and it seems like we currently don't have one")
+// testWSSServer returns a client hello info
+func testWSSServer(t *testing.T, listenAddr ma.Multiaddr) (ma.Multiaddr, peer.ID, chan error) {
+	errChan := make(chan error, 1)
+
+	ip := net.ParseIP("::")
+	tlsConf := getTLSConf(t, ip, time.Now(), time.Now().Add(time.Hour))
+	tlsConf.GetConfigForClient = func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
+		if chi.ServerName != "example.com" {
+			errChan <- fmt.Errorf("didn't get the expected sni")
+		}
+		return tlsConf, nil
 	}
-	raddr := ma.StringCast("/dns4/nyc-1.bootstrap.libp2p.io/tcp/443/wss")
-	rid, err := peer.Decode("QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm")
+
+	id, u := newSecureUpgrader(t)
+	tpt, err := New(u, network.NullResourceManager, WithTLSConfig(tlsConf))
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	tlsConfig := &tls.Config{InsecureSkipVerify: true}
-	_, u := newUpgrader(t)
-	tpt, err := New(u, network.NullResourceManager, WithTLSClientConfig(tlsConfig))
-	if err != nil {
-		t.Fatal(err)
+	// l, err := tpt.Listen(ma.StringCast("/ip4/127.0.0.1/tcp/0/wss"))
+	// l, err := tpt.Listen(ma.StringCast("/ip4/127.0.0.1/tcp/0/tls/ws"))
+	l, err := tpt.Listen(listenAddr)
+	fmt.Println("here", listenAddr)
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		l.Close()
+	})
+	go func() {
+		conn, err := l.Accept()
+		if err != nil {
+			errChan <- fmt.Errorf("error in accepting conn: %w", err)
+			return
+		}
+		defer conn.Close()
+
+		strm, err := conn.AcceptStream()
+		if err != nil {
+			errChan <- fmt.Errorf("error in accepting stream: %w", err)
+			return
+		}
+		defer strm.Close()
+		close(errChan)
+	}()
+
+	return l.Multiaddr(), id, errChan
+}
+
+func getTLSConf(t *testing.T, ip net.IP, start, end time.Time) *tls.Config {
+	t.Helper()
+	certTempl := &x509.Certificate{
+		SerialNumber:          big.NewInt(1234),
+		Subject:               pkix.Name{Organization: []string{"websocket"}},
+		NotBefore:             start,
+		NotAfter:              end,
+		IsCA:                  true,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		BasicConstraintsValid: true,
+		IPAddresses:           []net.IP{ip},
 	}
-	conn, err := tpt.Dial(context.Background(), raddr, rid)
-	if err != nil {
-		t.Fatal(err)
+	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	require.NoError(t, err)
+	caBytes, err := x509.CreateCertificate(rand.Reader, certTempl, certTempl, &priv.PublicKey, priv)
+	require.NoError(t, err)
+	cert, err := x509.ParseCertificate(caBytes)
+	require.NoError(t, err)
+	return &tls.Config{
+		Certificates: []tls.Certificate{{
+			Certificate: [][]byte{cert.Raw},
+			PrivateKey:  priv,
+			Leaf:        cert,
+		}},
 	}
+}
+
+func TestDialWss(t *testing.T) {
+	serverMA, rid, errChan := testWSSServer(t, ma.StringCast("/ip4/127.0.0.1/tcp/0/tls/sni/example.com/ws"))
+	require.Contains(t, serverMA.String(), "tls")
+
+	tlsConfig := &tls.Config{InsecureSkipVerify: true} // Our test server doesn't have a cert signed by a CA
+	_, u := newSecureUpgrader(t)
+	tpt, err := New(u, network.NullResourceManager, WithTLSClientConfig(tlsConfig))
+	require.NoError(t, err)
+
+	masToDial, err := tpt.Resolve(context.Background(), serverMA)
+	require.NoError(t, err)
+
+	conn, err := tpt.Dial(context.Background(), masToDial[0], rid)
+	require.NoError(t, err)
+	defer conn.Close()
+
 	stream, err := conn.OpenStream(context.Background())
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 	defer stream.Close()
+
+	err = <-errChan
+	require.NoError(t, err)
 }
 
 func TestWebsocketTransport(t *testing.T) {
@@ -160,9 +274,9 @@ func connectAndExchangeData(t *testing.T, laddr ma.Multiaddr, secure bool) {
 	l, err := tpt.Listen(laddr)
 	require.NoError(t, err)
 	if secure {
-		require.Equal(t, lastComponent(t, l.Multiaddr()), wssma)
+		require.Contains(t, l.Multiaddr().String(), "tls")
 	} else {
-		require.Equal(t, lastComponent(t, l.Multiaddr()), wsma)
+		require.Equal(t, lastComponent(t, l.Multiaddr()), wsComponent)
 	}
 	defer l.Close()
 
@@ -234,8 +348,8 @@ func TestWebsocketListenSecureAndInsecure(t *testing.T) {
 		conn, err := client.Dial(context.Background(), lnInsecure.Multiaddr(), serverID)
 		require.NoError(t, err)
 		defer conn.Close()
-		require.Equal(t, lastComponent(t, conn.RemoteMultiaddr()).String(), wsma.String())
-		require.Equal(t, lastComponent(t, conn.LocalMultiaddr()).String(), wsma.String())
+		require.Equal(t, lastComponent(t, conn.RemoteMultiaddr()).String(), wsComponent.String())
+		require.Equal(t, lastComponent(t, conn.LocalMultiaddr()).String(), wsComponent.String())
 
 		// dialing the secure address should fail
 		_, err = client.Dial(context.Background(), lnSecure.Multiaddr(), serverID)
@@ -251,8 +365,8 @@ func TestWebsocketListenSecureAndInsecure(t *testing.T) {
 		conn, err := client.Dial(context.Background(), lnSecure.Multiaddr(), serverID)
 		require.NoError(t, err)
 		defer conn.Close()
-		require.Equal(t, lastComponent(t, conn.RemoteMultiaddr()), wssma)
-		require.Equal(t, lastComponent(t, conn.LocalMultiaddr()), wssma)
+		require.Equal(t, lastComponent(t, conn.RemoteMultiaddr()), wssComponent)
+		require.Equal(t, lastComponent(t, conn.LocalMultiaddr()), wssComponent)
 
 		// dialing the insecure address should fail
 		_, err = client.Dial(context.Background(), lnInsecure.Multiaddr(), serverID)
@@ -346,3 +460,30 @@ func TestWriteZero(t *testing.T) {
 		t.Errorf("expected EOF, got err: %s", err)
 	}
 }
+
+func TestResolveMultiaddr(t *testing.T) {
+	// map[unresolved]resolved
+	testCases := map[string]string{
+		"/dns4/example.com/tcp/1234/wss":       "/dns4/example.com/tcp/1234/tls/sni/example.com/ws",
+		"/dns6/example.com/tcp/1234/wss":       "/dns6/example.com/tcp/1234/tls/sni/example.com/ws",
+		"/dnsaddr/example.com/tcp/1234/wss":    "/dnsaddr/example.com/tcp/1234/tls/sni/example.com/ws",
+		"/dns4/example.com/tcp/1234/tls/ws":    "/dns4/example.com/tcp/1234/tls/sni/example.com/ws",
+		"/dns6/example.com/tcp/1234/tls/ws":    "/dns6/example.com/tcp/1234/tls/sni/example.com/ws",
+		"/dnsaddr/example.com/tcp/1234/tls/ws": "/dnsaddr/example.com/tcp/1234/tls/sni/example.com/ws",
+	}
+
+	for unresolved, expectedMA := range testCases {
+		t.Run(unresolved, func(t *testing.T) {
+
+			m1 := ma.StringCast(unresolved)
+			wsTpt := WebsocketTransport{}
+			ctx := context.Background()
+
+			addrs, err := wsTpt.Resolve(ctx, m1)
+			require.NoError(t, err)
+			require.Len(t, addrs, 1)
+
+			require.Equal(t, expectedMA, addrs[0].String())
+		})
+	}
+}
diff --git a/p2p/transport/webtransport/cert_manager_test.go b/p2p/transport/webtransport/cert_manager_test.go
new file mode 100644
index 00000000..3f2328fb
--- /dev/null
+++ b/p2p/transport/webtransport/cert_manager_test.go
@@ -0,0 +1,102 @@
+package libp2pwebtransport
+
+import (
+	"crypto/sha256"
+	"crypto/tls"
+	"testing"
+	"time"
+
+	"github.com/benbjohnson/clock"
+	ma "github.com/multiformats/go-multiaddr"
+	"github.com/multiformats/go-multibase"
+	"github.com/multiformats/go-multihash"
+	"github.com/stretchr/testify/require"
+)
+
+func certificateHashFromTLSConfig(c *tls.Config) [32]byte {
+	return sha256.Sum256(c.Certificates[0].Certificate[0])
+}
+
+func splitMultiaddr(addr ma.Multiaddr) []ma.Component {
+	var components []ma.Component
+	ma.ForEach(addr, func(c ma.Component) bool {
+		components = append(components, c)
+		return true
+	})
+	return components
+}
+
+func certHashFromComponent(t *testing.T, comp ma.Component) []byte {
+	t.Helper()
+	_, data, err := multibase.Decode(comp.Value())
+	require.NoError(t, err)
+	mh, err := multihash.Decode(data)
+	require.NoError(t, err)
+	require.Equal(t, uint64(multihash.SHA2_256), mh.Code)
+	return mh.Digest
+}
+
+func TestInitialCert(t *testing.T) {
+	cl := clock.NewMock()
+	cl.Add(1234567 * time.Hour)
+	m, err := newCertManager(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.Equal(t, cert.Leaf.NotBefore.Add(certValidity), cert.Leaf.NotAfter)
+	addr := m.AddrComponent()
+	components := splitMultiaddr(addr)
+	require.Len(t, components, 2)
+	require.Equal(t, ma.P_CERTHASH, components[0].Protocol().Code)
+	hash := certificateHashFromTLSConfig(conf)
+	require.Equal(t, hash[:], certHashFromComponent(t, components[0]))
+	require.Equal(t, ma.P_CERTHASH, components[1].Protocol().Code)
+}
+
+func TestCertRenewal(t *testing.T) {
+	cl := clock.NewMock()
+	m, err := newCertManager(cl)
+	require.NoError(t, err)
+	defer m.Close()
+
+	firstConf := m.GetConfig()
+	first := splitMultiaddr(m.AddrComponent())
+	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)
+	require.Never(t, func() bool {
+		for i, c := range splitMultiaddr(m.AddrComponent()) {
+			if c.Value() != first[i].Value() {
+				return true
+			}
+		}
+		return false
+	}, 100*time.Millisecond, 10*time.Millisecond)
+	cl.Add(2 * time.Second)
+	require.Eventually(t, func() bool { return m.GetConfig() != firstConf }, 200*time.Millisecond, 10*time.Millisecond)
+	secondConf := m.GetConfig()
+
+	second := splitMultiaddr(m.AddrComponent())
+	require.Len(t, second, 2)
+	for _, c := range second {
+		require.Equal(t, ma.P_CERTHASH, c.Protocol().Code)
+	}
+	// check that the 2nd certificate from the beginning was rolled over to be the 1st certificate
+	require.Equal(t, first[1].Value(), second[0].Value())
+	require.NotEqual(t, first[0].Value(), second[1].Value())
+
+	cl.Add(certValidity - 2*clockSkewAllowance + time.Second)
+	require.Eventually(t, func() bool { return m.GetConfig() != secondConf }, 200*time.Millisecond, 10*time.Millisecond)
+	third := splitMultiaddr(m.AddrComponent())
+	require.Len(t, third, 2)
+	for _, c := range third {
+		require.Equal(t, ma.P_CERTHASH, c.Protocol().Code)
+	}
+	// 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())
+}
diff --git a/p2p/transport/webtransport/crypto_test.go b/p2p/transport/webtransport/crypto_test.go
new file mode 100644
index 00000000..d6d10620
--- /dev/null
+++ b/p2p/transport/webtransport/crypto_test.go
@@ -0,0 +1,132 @@
+package libp2pwebtransport
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha256"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"fmt"
+	"math/big"
+	mrand "math/rand"
+	"testing"
+	"time"
+
+	"github.com/multiformats/go-multihash"
+	"github.com/stretchr/testify/require"
+)
+
+func sha256Multihash(t *testing.T, b []byte) multihash.DecodedMultihash {
+	t.Helper()
+	hash := sha256.Sum256(b)
+	h, err := multihash.Encode(hash[:], multihash.SHA2_256)
+	require.NoError(t, err)
+	dh, err := multihash.Decode(h)
+	require.NoError(t, err)
+	return *dh
+}
+
+func generateCertWithKey(t *testing.T, key crypto.PrivateKey, start, end time.Time) *x509.Certificate {
+	t.Helper()
+	serial := int64(mrand.Uint64())
+	if serial < 0 {
+		serial = -serial
+	}
+	certTempl := &x509.Certificate{
+		SerialNumber:          big.NewInt(serial),
+		Subject:               pkix.Name{},
+		NotBefore:             start,
+		NotAfter:              end,
+		IsCA:                  true,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		BasicConstraintsValid: true,
+	}
+	caBytes, err := x509.CreateCertificate(rand.Reader, certTempl, certTempl, key.(interface{ Public() crypto.PublicKey }).Public(), key)
+	require.NoError(t, err)
+	ca, err := x509.ParseCertificate(caBytes)
+	require.NoError(t, err)
+	return ca
+}
+
+func TestCertificateVerification(t *testing.T) {
+	now := time.Now()
+	ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	require.NoError(t, err)
+	rsaKey, err := rsa.GenerateKey(rand.Reader, 1024)
+	require.NoError(t, err)
+
+	t.Run("accepting a valid cert", func(t *testing.T) {
+		validCert := generateCertWithKey(t, ecdsaKey, now, now.Add(14*24*time.Hour))
+		require.NoError(t, verifyRawCerts([][]byte{validCert.Raw}, []multihash.DecodedMultihash{sha256Multihash(t, validCert.Raw)}))
+	})
+
+	for _, tc := range [...]struct {
+		name   string
+		cert   *x509.Certificate
+		errStr string
+	}{
+		{
+			name:   "validitity period too long",
+			cert:   generateCertWithKey(t, ecdsaKey, now, now.Add(15*24*time.Hour)),
+			errStr: "cert must not be valid for longer than 14 days",
+		},
+		{
+			name:   "uses RSA key",
+			cert:   generateCertWithKey(t, rsaKey, now, now.Add(14*24*time.Hour)),
+			errStr: "RSA",
+		},
+		{
+			name:   "expired certificate",
+			cert:   generateCertWithKey(t, ecdsaKey, now.Add(-14*24*time.Hour), now),
+			errStr: "cert not valid",
+		},
+		{
+			name:   "not yet valid",
+			cert:   generateCertWithKey(t, ecdsaKey, now.Add(time.Hour), now.Add(time.Hour+14*24*time.Hour)),
+			errStr: "cert not valid",
+		},
+	} {
+		tc := tc
+		t.Run(fmt.Sprintf("rejecting invalid certificates: %s", tc.name), func(t *testing.T) {
+			err := verifyRawCerts([][]byte{tc.cert.Raw}, []multihash.DecodedMultihash{sha256Multihash(t, tc.cert.Raw)})
+			require.Error(t, err)
+			require.Contains(t, err.Error(), tc.errStr)
+		})
+	}
+
+	for _, tc := range [...]struct {
+		name   string
+		certs  [][]byte
+		hashes []multihash.DecodedMultihash
+		errStr string
+	}{
+		{
+			name:   "no certificates",
+			hashes: []multihash.DecodedMultihash{sha256Multihash(t, []byte("foobar"))},
+			errStr: "no cert",
+		},
+		{
+			name:   "certificate not parseable",
+			certs:  [][]byte{[]byte("foobar")},
+			hashes: []multihash.DecodedMultihash{sha256Multihash(t, []byte("foobar"))},
+			errStr: "x509: malformed certificate",
+		},
+		{
+			name:   "hash mismatch",
+			certs:  [][]byte{generateCertWithKey(t, ecdsaKey, now, now.Add(15*24*time.Hour)).Raw},
+			hashes: []multihash.DecodedMultihash{sha256Multihash(t, []byte("foobar"))},
+			errStr: "cert hash not found",
+		},
+	} {
+		tc := tc
+		t.Run(fmt.Sprintf("rejecting invalid certificates: %s", tc.name), func(t *testing.T) {
+			err := verifyRawCerts(tc.certs, tc.hashes)
+			require.Error(t, err)
+			require.Contains(t, err.Error(), tc.errStr)
+		})
+	}
+}
diff --git a/p2p/transport/webtransport/mock_connection_gater_test.go b/p2p/transport/webtransport/mock_connection_gater_test.go
new file mode 100644
index 00000000..c6e7dbaa
--- /dev/null
+++ b/p2p/transport/webtransport/mock_connection_gater_test.go
@@ -0,0 +1,109 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/libp2p/go-libp2p/core/connmgr (interfaces: ConnectionGater)
+
+// Package libp2pwebtransport_test is a generated GoMock package.
+package libp2pwebtransport_test
+
+import (
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	control "github.com/libp2p/go-libp2p/core/control"
+	network "github.com/libp2p/go-libp2p/core/network"
+	peer "github.com/libp2p/go-libp2p/core/peer"
+	multiaddr "github.com/multiformats/go-multiaddr"
+)
+
+// MockConnectionGater is a mock of ConnectionGater interface.
+type MockConnectionGater struct {
+	ctrl     *gomock.Controller
+	recorder *MockConnectionGaterMockRecorder
+}
+
+// MockConnectionGaterMockRecorder is the mock recorder for MockConnectionGater.
+type MockConnectionGaterMockRecorder struct {
+	mock *MockConnectionGater
+}
+
+// NewMockConnectionGater creates a new mock instance.
+func NewMockConnectionGater(ctrl *gomock.Controller) *MockConnectionGater {
+	mock := &MockConnectionGater{ctrl: ctrl}
+	mock.recorder = &MockConnectionGaterMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockConnectionGater) EXPECT() *MockConnectionGaterMockRecorder {
+	return m.recorder
+}
+
+// InterceptAccept mocks base method.
+func (m *MockConnectionGater) InterceptAccept(arg0 network.ConnMultiaddrs) bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InterceptAccept", arg0)
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// InterceptAccept indicates an expected call of InterceptAccept.
+func (mr *MockConnectionGaterMockRecorder) InterceptAccept(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptAccept", reflect.TypeOf((*MockConnectionGater)(nil).InterceptAccept), arg0)
+}
+
+// InterceptAddrDial mocks base method.
+func (m *MockConnectionGater) InterceptAddrDial(arg0 peer.ID, arg1 multiaddr.Multiaddr) bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InterceptAddrDial", arg0, arg1)
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// InterceptAddrDial indicates an expected call of InterceptAddrDial.
+func (mr *MockConnectionGaterMockRecorder) InterceptAddrDial(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptAddrDial", reflect.TypeOf((*MockConnectionGater)(nil).InterceptAddrDial), arg0, arg1)
+}
+
+// InterceptPeerDial mocks base method.
+func (m *MockConnectionGater) InterceptPeerDial(arg0 peer.ID) bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InterceptPeerDial", arg0)
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// InterceptPeerDial indicates an expected call of InterceptPeerDial.
+func (mr *MockConnectionGaterMockRecorder) InterceptPeerDial(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptPeerDial", reflect.TypeOf((*MockConnectionGater)(nil).InterceptPeerDial), arg0)
+}
+
+// InterceptSecured mocks base method.
+func (m *MockConnectionGater) InterceptSecured(arg0 network.Direction, arg1 peer.ID, arg2 network.ConnMultiaddrs) bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InterceptSecured", arg0, arg1, arg2)
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// InterceptSecured indicates an expected call of InterceptSecured.
+func (mr *MockConnectionGaterMockRecorder) InterceptSecured(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptSecured", reflect.TypeOf((*MockConnectionGater)(nil).InterceptSecured), arg0, arg1, arg2)
+}
+
+// InterceptUpgraded mocks base method.
+func (m *MockConnectionGater) InterceptUpgraded(arg0 network.Conn) (bool, control.DisconnectReason) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InterceptUpgraded", arg0)
+	ret0, _ := ret[0].(bool)
+	ret1, _ := ret[1].(control.DisconnectReason)
+	return ret0, ret1
+}
+
+// InterceptUpgraded indicates an expected call of InterceptUpgraded.
+func (mr *MockConnectionGaterMockRecorder) InterceptUpgraded(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptUpgraded", reflect.TypeOf((*MockConnectionGater)(nil).InterceptUpgraded), arg0)
+}
diff --git a/p2p/transport/webtransport/multiaddr_test.go b/p2p/transport/webtransport/multiaddr_test.go
new file mode 100644
index 00000000..3af1ce56
--- /dev/null
+++ b/p2p/transport/webtransport/multiaddr_test.go
@@ -0,0 +1,76 @@
+package libp2pwebtransport
+
+import (
+	"fmt"
+	"net"
+	"testing"
+
+	ma "github.com/multiformats/go-multiaddr"
+	"github.com/multiformats/go-multibase"
+	"github.com/multiformats/go-multihash"
+	"github.com/stretchr/testify/require"
+)
+
+func TestWebtransportMultiaddr(t *testing.T) {
+	t.Run("valid", func(t *testing.T) {
+		addr, err := toWebtransportMultiaddr(&net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1337})
+		require.NoError(t, err)
+		require.Equal(t, "/ip4/127.0.0.1/udp/1337/quic/webtransport", addr.String())
+	})
+
+	t.Run("invalid", func(t *testing.T) {
+		_, err := toWebtransportMultiaddr(&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1337})
+		require.EqualError(t, err, "not a UDP address")
+	})
+}
+
+func TestWebtransportMultiaddrFromString(t *testing.T) {
+	t.Run("valid", func(t *testing.T) {
+		addr, err := stringToWebtransportMultiaddr("1.2.3.4:60042")
+		require.NoError(t, err)
+		require.Equal(t, "/ip4/1.2.3.4/udp/60042/quic/webtransport", addr.String())
+	})
+
+	t.Run("invalid", func(t *testing.T) {
+		for _, addr := range [...]string{
+			"1.2.3.4",        // missing port
+			"1.2.3.4:123456", // invalid port
+			":1234",          // missing IP
+			"foobar",
+		} {
+			_, err := stringToWebtransportMultiaddr(addr)
+			require.Error(t, err)
+		}
+	})
+}
+
+func encodeCertHash(t *testing.T, b []byte, mh uint64, mb multibase.Encoding) string {
+	t.Helper()
+	h, err := multihash.Encode(b, mh)
+	require.NoError(t, err)
+	str, err := multibase.Encode(mb, h)
+	require.NoError(t, err)
+	return str
+}
+
+func TestExtractCertHashes(t *testing.T) {
+	fooHash := encodeCertHash(t, []byte("foo"), multihash.SHA2_256, multibase.Base58BTC)
+	barHash := encodeCertHash(t, []byte("bar"), multihash.BLAKE2B_MAX, multibase.Base32)
+
+	// valid cases
+	for _, tc := range [...]struct {
+		addr   string
+		hashes []string
+	}{
+		{addr: "/ip4/127.0.0.1/udp/1234/quic/webtransport"},
+		{addr: fmt.Sprintf("/ip4/127.0.0.1/udp/1234/quic/webtransport/certhash/%s", fooHash), hashes: []string{"foo"}},
+		{addr: fmt.Sprintf("/ip4/127.0.0.1/udp/1234/quic/webtransport/certhash/%s/certhash/%s", fooHash, barHash), hashes: []string{"foo", "bar"}},
+	} {
+		ch, err := extractCertHashes(ma.StringCast(tc.addr))
+		require.NoError(t, err)
+		require.Len(t, ch, len(tc.hashes))
+		for i, h := range tc.hashes {
+			require.Equal(t, h, string(ch[i].Digest))
+		}
+	}
+}
diff --git a/p2p/transport/webtransport/transport_test.go b/p2p/transport/webtransport/transport_test.go
new file mode 100644
index 00000000..92680faf
--- /dev/null
+++ b/p2p/transport/webtransport/transport_test.go
@@ -0,0 +1,543 @@
+package libp2pwebtransport_test
+
+import (
+	"context"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/sha256"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"errors"
+	"fmt"
+	"io"
+	"math/big"
+	"net"
+	"strings"
+	"testing"
+	"time"
+
+	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"
+	tpt "github.com/libp2p/go-libp2p/core/transport"
+	libp2pwebtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
+
+	"github.com/golang/mock/gomock"
+	ma "github.com/multiformats/go-multiaddr"
+	manet "github.com/multiformats/go-multiaddr/net"
+	"github.com/multiformats/go-multibase"
+	"github.com/multiformats/go-multihash"
+	"github.com/stretchr/testify/require"
+)
+
+func newIdentity(t *testing.T) (peer.ID, ic.PrivKey) {
+	key, _, err := ic.GenerateEd25519Key(rand.Reader)
+	require.NoError(t, err)
+	id, err := peer.IDFromPrivateKey(key)
+	require.NoError(t, err)
+	return id, key
+}
+
+func randomMultihash(t *testing.T) string {
+	t.Helper()
+	b := make([]byte, 16)
+	rand.Read(b)
+	h, err := multihash.Encode(b, multihash.KECCAK_224)
+	require.NoError(t, err)
+	s, err := multibase.Encode(multibase.Base32hex, h)
+	require.NoError(t, err)
+	return s
+}
+
+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 stripCertHashes(addr ma.Multiaddr) ma.Multiaddr {
+	for {
+		_, err := addr.ValueForProtocol(ma.P_CERTHASH)
+		if err != nil {
+			return addr
+		}
+		addr, _ = ma.SplitLast(addr)
+	}
+}
+
+// create a /certhash multiaddr component using the SHA256 of foobar
+func getCerthashComponent(t *testing.T, b []byte) ma.Multiaddr {
+	t.Helper()
+	h := sha256.Sum256(b)
+	mh, err := multihash.Encode(h[:], multihash.SHA2_256)
+	require.NoError(t, err)
+	certStr, err := multibase.Encode(multibase.Base58BTC, mh)
+	require.NoError(t, err)
+	ha, err := ma.NewComponent(ma.ProtocolWithCode(ma.P_CERTHASH).Name, certStr)
+	require.NoError(t, err)
+	return ha
+}
+
+func TestTransport(t *testing.T) {
+	serverID, serverKey := newIdentity(t)
+	tr, err := libp2pwebtransport.New(serverKey, nil, network.NullResourceManager)
+	require.NoError(t, err)
+	defer tr.(io.Closer).Close()
+	ln, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	require.NoError(t, err)
+	defer ln.Close()
+
+	addrChan := make(chan ma.Multiaddr)
+	go func() {
+		_, clientKey := newIdentity(t)
+		tr2, err := libp2pwebtransport.New(clientKey, nil, network.NullResourceManager)
+		require.NoError(t, err)
+		defer tr2.(io.Closer).Close()
+
+		conn, err := tr2.Dial(context.Background(), ln.Multiaddr(), serverID)
+		require.NoError(t, err)
+		str, err := conn.OpenStream(context.Background())
+		require.NoError(t, err)
+		_, err = str.Write([]byte("foobar"))
+		require.NoError(t, err)
+		require.NoError(t, str.Close())
+
+		// check RemoteMultiaddr
+		_, addr, err := manet.DialArgs(ln.Multiaddr())
+		require.NoError(t, err)
+		_, port, err := net.SplitHostPort(addr)
+		require.NoError(t, err)
+		require.Equal(t, ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/udp/%s/quic/webtransport", port)), conn.RemoteMultiaddr())
+		addrChan <- conn.RemoteMultiaddr()
+	}()
+
+	conn, err := ln.Accept()
+	require.NoError(t, err)
+	require.False(t, conn.IsClosed())
+	str, err := conn.AcceptStream()
+	require.NoError(t, err)
+	data, err := io.ReadAll(str)
+	require.NoError(t, err)
+	require.Equal(t, "foobar", string(data))
+	require.Equal(t, <-addrChan, conn.LocalMultiaddr())
+	require.NoError(t, conn.Close())
+	require.True(t, conn.IsClosed())
+}
+
+func TestHashVerification(t *testing.T) {
+	serverID, serverKey := newIdentity(t)
+	tr, err := libp2pwebtransport.New(serverKey, nil, network.NullResourceManager)
+	require.NoError(t, err)
+	defer tr.(io.Closer).Close()
+	ln, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	require.NoError(t, err)
+	done := make(chan struct{})
+	go func() {
+		defer close(done)
+		_, err := ln.Accept()
+		require.Error(t, err)
+	}()
+
+	_, clientKey := newIdentity(t)
+	tr2, err := libp2pwebtransport.New(clientKey, nil, network.NullResourceManager)
+	require.NoError(t, err)
+	defer tr2.(io.Closer).Close()
+
+	foobarHash := getCerthashComponent(t, []byte("foobar"))
+
+	t.Run("fails using only a wrong hash", func(t *testing.T) {
+		// replace the certificate hash in the multiaddr with a fake hash
+		addr := stripCertHashes(ln.Multiaddr()).Encapsulate(foobarHash)
+		_, err := tr2.Dial(context.Background(), addr, serverID)
+		require.Error(t, err)
+		require.Contains(t, err.Error(), "CRYPTO_ERROR (0x12a): cert hash not found")
+	})
+
+	t.Run("fails when adding a wrong hash", func(t *testing.T) {
+		_, err := tr2.Dial(context.Background(), ln.Multiaddr().Encapsulate(foobarHash), serverID)
+		require.Error(t, err)
+	})
+
+	require.NoError(t, ln.Close())
+	<-done
+}
+
+func TestCanDial(t *testing.T) {
+	valid := []ma.Multiaddr{
+		ma.StringCast("/ip4/127.0.0.1/udp/1234/quic/webtransport/certhash/" + randomMultihash(t)),
+		ma.StringCast("/ip6/b16b:8255:efc6:9cd5:1a54:ee86:2d7a:c2e6/udp/1234/quic/webtransport/certhash/" + randomMultihash(t)),
+		ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/udp/1234/quic/webtransport/certhash/%s/certhash/%s/certhash/%s", randomMultihash(t), randomMultihash(t), randomMultihash(t))),
+		ma.StringCast("/ip4/127.0.0.1/udp/1234/quic/webtransport"), // no certificate hash
+	}
+
+	invalid := []ma.Multiaddr{
+		ma.StringCast("/ip4/127.0.0.1/udp/1234"),              // missing webtransport
+		ma.StringCast("/ip4/127.0.0.1/udp/1234/webtransport"), // missing quic
+		ma.StringCast("/ip4/127.0.0.1/tcp/1234/webtransport"), // WebTransport over TCP? Is this a joke?
+	}
+
+	_, key := newIdentity(t)
+	tr, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+	require.NoError(t, err)
+	defer tr.(io.Closer).Close()
+
+	for _, addr := range valid {
+		require.Truef(t, tr.CanDial(addr), "expected to be able to dial %s", addr)
+	}
+	for _, addr := range invalid {
+		require.Falsef(t, tr.CanDial(addr), "expected to not be able to dial %s", addr)
+	}
+}
+
+func TestListenAddrValidity(t *testing.T) {
+	valid := []ma.Multiaddr{
+		ma.StringCast("/ip6/::/udp/0/quic/webtransport/"),
+		ma.StringCast("/ip4/127.0.0.1/udp/1234/quic/webtransport/"),
+	}
+
+	invalid := []ma.Multiaddr{
+		ma.StringCast("/ip4/127.0.0.1/udp/1234"),              // missing webtransport
+		ma.StringCast("/ip4/127.0.0.1/udp/1234/webtransport"), // missing quic
+		ma.StringCast("/ip4/127.0.0.1/tcp/1234/webtransport"), // WebTransport over TCP? Is this a joke?
+		ma.StringCast("/ip4/127.0.0.1/udp/1234/quic/webtransport/certhash/" + randomMultihash(t)),
+	}
+
+	_, key := newIdentity(t)
+	tr, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+	require.NoError(t, err)
+	defer tr.(io.Closer).Close()
+
+	for _, addr := range valid {
+		ln, err := tr.Listen(addr)
+		require.NoErrorf(t, err, "expected to be able to listen on %s", addr)
+		ln.Close()
+	}
+	for _, addr := range invalid {
+		_, err := tr.Listen(addr)
+		require.Errorf(t, err, "expected to not be able to listen on %s", addr)
+	}
+}
+
+func TestListenerAddrs(t *testing.T) {
+	_, key := newIdentity(t)
+	tr, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+	require.NoError(t, err)
+	defer tr.(io.Closer).Close()
+
+	ln1, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	require.NoError(t, err)
+	ln2, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	require.NoError(t, err)
+	hashes1 := extractCertHashes(ln1.Multiaddr())
+	require.Len(t, hashes1, 2)
+	hashes2 := extractCertHashes(ln2.Multiaddr())
+	require.Equal(t, hashes1, hashes2)
+}
+
+func TestResourceManagerDialing(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	rcmgr := mocknetwork.NewMockResourceManager(ctrl)
+
+	addr := ma.StringCast("/ip4/9.8.7.6/udp/1234/quic/webtransport")
+	p := peer.ID("foobar")
+
+	_, key := newIdentity(t)
+	tr, err := libp2pwebtransport.New(key, nil, rcmgr)
+	require.NoError(t, err)
+	defer tr.(io.Closer).Close()
+
+	scope := mocknetwork.NewMockConnManagementScope(ctrl)
+	rcmgr.EXPECT().OpenConnection(network.DirOutbound, false, addr).Return(scope, nil)
+	scope.EXPECT().SetPeer(p).Return(errors.New("denied"))
+	scope.EXPECT().Done()
+
+	_, err = tr.Dial(context.Background(), addr, p)
+	require.EqualError(t, err, "denied")
+}
+
+func TestResourceManagerListening(t *testing.T) {
+	clientID, key := newIdentity(t)
+	cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+	require.NoError(t, err)
+	defer cl.(io.Closer).Close()
+
+	t.Run("blocking the connection", func(t *testing.T) {
+		serverID, key := newIdentity(t)
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+		rcmgr := mocknetwork.NewMockResourceManager(ctrl)
+		tr, err := libp2pwebtransport.New(key, nil, rcmgr)
+		require.NoError(t, err)
+		ln, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+		require.NoError(t, err)
+		defer ln.Close()
+
+		rcmgr.EXPECT().OpenConnection(network.DirInbound, false, gomock.Any()).DoAndReturn(func(_ network.Direction, _ bool, addr ma.Multiaddr) (network.ConnManagementScope, error) {
+			_, err := addr.ValueForProtocol(ma.P_WEBTRANSPORT)
+			require.NoError(t, err, "expected a WebTransport multiaddr")
+			_, addrStr, err := manet.DialArgs(addr)
+			require.NoError(t, err)
+			host, _, err := net.SplitHostPort(addrStr)
+			require.NoError(t, err)
+			require.Equal(t, "127.0.0.1", host)
+			return nil, errors.New("denied")
+		})
+
+		_, err = cl.Dial(context.Background(), ln.Multiaddr(), serverID)
+		require.EqualError(t, err, "received status 503")
+	})
+
+	t.Run("blocking the peer", func(t *testing.T) {
+		serverID, key := newIdentity(t)
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+		rcmgr := mocknetwork.NewMockResourceManager(ctrl)
+		tr, err := libp2pwebtransport.New(key, nil, rcmgr)
+		require.NoError(t, err)
+		ln, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+		require.NoError(t, err)
+		defer ln.Close()
+
+		serverDone := make(chan struct{})
+		scope := mocknetwork.NewMockConnManagementScope(ctrl)
+		rcmgr.EXPECT().OpenConnection(network.DirInbound, false, gomock.Any()).Return(scope, nil)
+		scope.EXPECT().SetPeer(clientID).Return(errors.New("denied"))
+		scope.EXPECT().Done().Do(func() { close(serverDone) })
+
+		// The handshake will complete, but the server will immediately close the connection.
+		conn, err := cl.Dial(context.Background(), ln.Multiaddr(), serverID)
+		require.NoError(t, err)
+		defer conn.Close()
+		clientDone := make(chan struct{})
+		go func() {
+			defer close(clientDone)
+			_, err = conn.AcceptStream()
+			require.Error(t, err)
+		}()
+		select {
+		case <-clientDone:
+		case <-time.After(5 * time.Second):
+			t.Fatal("timeout")
+		}
+		select {
+		case <-serverDone:
+		case <-time.After(5 * time.Second):
+			t.Fatal("timeout")
+		}
+	})
+}
+
+// TODO: unify somehow. We do the same in libp2pquic.
+//go:generate sh -c "mockgen -package libp2pwebtransport_test -destination mock_connection_gater_test.go github.com/libp2p/go-libp2p/core/connmgr ConnectionGater && goimports -w mock_connection_gater_test.go"
+
+func TestConnectionGaterDialing(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	connGater := NewMockConnectionGater(ctrl)
+
+	serverID, serverKey := newIdentity(t)
+	tr, err := libp2pwebtransport.New(serverKey, nil, network.NullResourceManager)
+	require.NoError(t, err)
+	defer tr.(io.Closer).Close()
+	ln, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	require.NoError(t, err)
+	defer ln.Close()
+
+	connGater.EXPECT().InterceptSecured(network.DirOutbound, serverID, gomock.Any()).Do(func(_ network.Direction, _ peer.ID, addrs network.ConnMultiaddrs) {
+		require.Equal(t, stripCertHashes(ln.Multiaddr()), addrs.RemoteMultiaddr())
+	})
+	_, key := newIdentity(t)
+	cl, err := libp2pwebtransport.New(key, connGater, network.NullResourceManager)
+	require.NoError(t, err)
+	defer cl.(io.Closer).Close()
+	_, err = cl.Dial(context.Background(), ln.Multiaddr(), serverID)
+	require.EqualError(t, err, "secured connection gated")
+}
+
+func TestConnectionGaterInterceptAccept(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	connGater := NewMockConnectionGater(ctrl)
+
+	serverID, serverKey := newIdentity(t)
+	tr, err := libp2pwebtransport.New(serverKey, connGater, network.NullResourceManager)
+	require.NoError(t, err)
+	defer tr.(io.Closer).Close()
+	ln, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	require.NoError(t, err)
+	defer ln.Close()
+
+	connGater.EXPECT().InterceptAccept(gomock.Any()).Do(func(addrs network.ConnMultiaddrs) {
+		require.Equal(t, stripCertHashes(ln.Multiaddr()), addrs.LocalMultiaddr())
+		require.NotEqual(t, stripCertHashes(ln.Multiaddr()), addrs.RemoteMultiaddr())
+	})
+
+	_, key := newIdentity(t)
+	cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+	require.NoError(t, err)
+	defer cl.(io.Closer).Close()
+	_, err = cl.Dial(context.Background(), ln.Multiaddr(), serverID)
+	require.EqualError(t, err, "received status 403")
+}
+
+func TestConnectionGaterInterceptSecured(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	connGater := NewMockConnectionGater(ctrl)
+
+	serverID, serverKey := newIdentity(t)
+	tr, err := libp2pwebtransport.New(serverKey, connGater, network.NullResourceManager)
+	require.NoError(t, err)
+	defer tr.(io.Closer).Close()
+	ln, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	require.NoError(t, err)
+	defer ln.Close()
+
+	clientID, key := newIdentity(t)
+	cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+	require.NoError(t, err)
+	defer cl.(io.Closer).Close()
+
+	connGater.EXPECT().InterceptAccept(gomock.Any()).Return(true)
+	connGater.EXPECT().InterceptSecured(network.DirInbound, clientID, gomock.Any()).Do(func(_ network.Direction, _ peer.ID, addrs network.ConnMultiaddrs) {
+		require.Equal(t, stripCertHashes(ln.Multiaddr()), addrs.LocalMultiaddr())
+		require.NotEqual(t, stripCertHashes(ln.Multiaddr()), addrs.RemoteMultiaddr())
+	})
+	// The handshake will complete, but the server will immediately close the connection.
+	conn, err := cl.Dial(context.Background(), ln.Multiaddr(), serverID)
+	require.NoError(t, err)
+	defer conn.Close()
+	done := make(chan struct{})
+	go func() {
+		defer close(done)
+		_, err = conn.AcceptStream()
+		require.Error(t, err)
+	}()
+	select {
+	case <-done:
+	case <-time.After(5 * time.Second):
+		t.Fatal("timeout")
+	}
+}
+
+func getTLSConf(t *testing.T, ip net.IP, start, end time.Time) *tls.Config {
+	t.Helper()
+	certTempl := &x509.Certificate{
+		SerialNumber:          big.NewInt(1234),
+		Subject:               pkix.Name{Organization: []string{"webtransport"}},
+		NotBefore:             start,
+		NotAfter:              end,
+		IsCA:                  true,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		BasicConstraintsValid: true,
+		IPAddresses:           []net.IP{ip},
+	}
+	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	require.NoError(t, err)
+	caBytes, err := x509.CreateCertificate(rand.Reader, certTempl, certTempl, &priv.PublicKey, priv)
+	require.NoError(t, err)
+	cert, err := x509.ParseCertificate(caBytes)
+	require.NoError(t, err)
+	return &tls.Config{
+		Certificates: []tls.Certificate{{
+			Certificate: [][]byte{cert.Raw},
+			PrivateKey:  priv,
+			Leaf:        cert,
+		}},
+	}
+}
+
+func TestStaticTLSConf(t *testing.T) {
+	tlsConf := getTLSConf(t, net.ParseIP("127.0.0.1"), time.Now(), time.Now().Add(365*24*time.Hour))
+
+	serverID, serverKey := newIdentity(t)
+	tr, err := libp2pwebtransport.New(serverKey, nil, network.NullResourceManager, libp2pwebtransport.WithTLSConfig(tlsConf))
+	require.NoError(t, err)
+	defer tr.(io.Closer).Close()
+	ln, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	require.NoError(t, err)
+	defer ln.Close()
+	require.Empty(t, extractCertHashes(ln.Multiaddr()), "listener address shouldn't contain any certhash")
+
+	t.Run("fails when the certificate is invalid", func(t *testing.T) {
+		_, key := newIdentity(t)
+		cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+		require.NoError(t, err)
+		defer cl.(io.Closer).Close()
+
+		_, err = cl.Dial(context.Background(), ln.Multiaddr(), serverID)
+		require.Error(t, err)
+		if !strings.Contains(err.Error(), "certificate is not trusted") &&
+			!strings.Contains(err.Error(), "certificate signed by unknown authority") {
+			t.Fatalf("expected a certificate error, got %+v", err)
+		}
+	})
+
+	t.Run("fails when dialing with a wrong certhash", func(t *testing.T) {
+		_, key := newIdentity(t)
+		cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+		require.NoError(t, err)
+		defer cl.(io.Closer).Close()
+
+		addr := ln.Multiaddr().Encapsulate(getCerthashComponent(t, []byte("foo")))
+		_, err = cl.Dial(context.Background(), addr, serverID)
+		require.Error(t, err)
+		require.Contains(t, err.Error(), "cert hash not found")
+	})
+
+	t.Run("accepts a valid TLS certificate", func(t *testing.T) {
+		_, key := newIdentity(t)
+		store := x509.NewCertPool()
+		store.AddCert(tlsConf.Certificates[0].Leaf)
+		tlsConf := &tls.Config{RootCAs: store}
+		cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager, libp2pwebtransport.WithTLSClientConfig(tlsConf))
+		require.NoError(t, err)
+		defer cl.(io.Closer).Close()
+
+		require.True(t, cl.CanDial(ln.Multiaddr()))
+		conn, err := cl.Dial(context.Background(), ln.Multiaddr(), serverID)
+		require.NoError(t, err)
+		defer conn.Close()
+	})
+}
+
+func TestAcceptQueueFilledUp(t *testing.T) {
+	serverID, serverKey := newIdentity(t)
+	tr, err := libp2pwebtransport.New(serverKey, nil, network.NullResourceManager)
+	require.NoError(t, err)
+	defer tr.(io.Closer).Close()
+	ln, err := tr.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic/webtransport"))
+	require.NoError(t, err)
+	defer ln.Close()
+
+	newConn := func() (tpt.CapableConn, error) {
+		t.Helper()
+		_, key := newIdentity(t)
+		cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+		require.NoError(t, err)
+		defer cl.(io.Closer).Close()
+		return cl.Dial(context.Background(), ln.Multiaddr(), serverID)
+	}
+
+	for i := 0; i < 16; i++ {
+		conn, err := newConn()
+		require.NoError(t, err)
+		defer conn.Close()
+	}
+
+	conn, err := newConn()
+	if err == nil {
+		_, err = conn.AcceptStream()
+	}
+	require.Error(t, err)
+}