Repository: libp2p/go-libp2p
Pull request #1813 (2022-10-07 21:00:34)

noise: use Noise Extension to negotiate the muxer during the handshake
Muxer negotiation using early data of Noise handshake. 
This is the Noise implementation of issue #1789  

marten-seemann: @julian88110 Could you rebase (`git rebase`) this PR on top of the current `master`? Looks like there are still commits from your earlier TLS change in the commit history here.

Patch:
diff --git a/ROADMAP.md b/ROADMAP.md
index 63f97ab9..3d4fde81 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,95 +1,164 @@
-
-# go-libp2p roadmap Q4’22/Q1’23
+# go-libp2p roadmap Q4’22/Q1’23 <!-- omit in toc -->
 
 ```
-Date: 2022-10-07
-Status: In Progress
-Notes: This document is still in review will may be heavily modified based on stakeholder feedback. Please add any feedback or questions in:
+Date: 2022-10-20
+Status: Accepted
+Notes: Internal go-libp2p stakeholders have aligned on this roadmap. Please add any feedback or questions in:
 https://github.com/libp2p/go-libp2p/issues/1806
 ```
 
+## Table of Contents <!-- omit in toc -->
+
+- [About the Roadmap](#about-the-roadmap)
+  - [Vision](#vision)
+  - [Sections](#sections)
+  - [Done criteria](#done-criteria)
+- [Benchmarking and Testing](#benchmarking-and-testing)
+- [🛣️ Milestones](#️-milestones)
+  - [2022](#2022)
+    - [Early Q4 (October)](#early-q4-october)
+    - [Mid Q4 (November)](#mid-q4-november)
+    - [End of Q4 (December)](#end-of-q4-december)
+  - [2023](#2023)
+    - [Early Q1 (January)](#early-q1-january)
+    - [Mid Q1 (February)](#mid-q1-february)
+    - [End of Q1 (March)](#end-of-q1-march)
+  - [Up Next](#up-next)
+- [📖 Appendix](#-appendix)
+  - [A. 📺 Universal Browser Connectivity](#a--universal-browser-connectivity)
+    - [1. WebRTC: Browser to Server](#1-webrtc-browser-to-server)
+    - [2. WebRTC: Browser to Browser](#2-webrtc-browser-to-browser)
+    - [3. WebTransport: Update to new draft versions](#3-webtransport-update-to-new-draft-versions)
+  - [B. ⚡️ Handshakes at the Speed of Light](#b-️-handshakes-at-the-speed-of-light)
+    - [1. Early Muxer Negotiation](#1-early-muxer-negotiation)
+    - [2. Adding security protocol](#2-adding-security-protocol)
+    - [3. 0.5 RTT data optimization](#3-05-rtt-data-optimization)
+  - [C. 🧠 Smart Dialing](#c--smart-dialing)
+    - [1. Happy Eyeballs](#1-happy-eyeballs)
+    - [2. QUIC Blackhole detector](#2-quic-blackhole-detector)
+    - [3. RTT estimation](#3-rtt-estimation)
+  - [D. 📊 Comprehensive Metrics](#d--comprehensive-metrics)
+  - [E. 📢 Judicious Address Advertisements](#e--judicious-address-advertisements)
+
 ## About the Roadmap
+
+### Vision
+We, the maintainers, are committed to upholding libp2p's shared core tenets and ensuring go-libp2p is: [**Secure, Stable, Specified, and Performant.**](https://github.com/libp2p/specs/blob/master/ROADMAP.md#core-tenets)
+
+Roadmap items in this document were sourced in part from the [overarching libp2p project roadmap.](https://github.com/libp2p/specs/blob/master/ROADMAP.md)
+
 ### Sections
-This document consists of two sections: [Milestones](#Milestones) and the [Roadmap Appendix](#Roadmap-Appendix)
+This document consists of two sections: [Milestones](#️-milestones) and the [Appendix](#-appendix)
+
+[Milestones](#️-milestones) is our best educated guess (not a hard commitment) around when we plan to ship the key features.
+Where possible projects are broken down into discrete sub-projects e.g. project "A" may contain two sub-projects: A.1 and A.2
 
-[Milestones](#Milestones) is our best educated guess (not a hard commitment) around when we plan to ship the key features.
-Where possible we've broken down a project into discrete sub-projects e.g. project "A" (⚡Handshakes at the Speed of Light) contains three sub-projects and consists of A.1, A.2, and A.3
 A project is signified as "complete" once all of it's sub-projects are shipped.
 
-The [Roadmap Appendix](#Roadmap-Appendix) section describes each project in detail and lists its sub-projects (if applicable.) Here you can find the motivation for each project and goals.
+The [Appendix](#-appendix) section describes a project's high-level motivation, goals, and lists sub-projects.
 
-We've deep-linked each section header to it's corresponding GitHub Epic. Latest information on the progress of each project there can be found in the Epics and their child issues (as we will make regular updates.)
+Each Appendix header is linked to a GitHub Epic. Latest information on progress can be found in the Epics and child issues.
 
 ### Done criteria
-Our "Definition of Done" for projects/sub-projects that involve writing new protocols or modifications to existing ones will usually consist of the following:
-- [ ] Spec is merged and classified as "Candidate Recommendation"
-- [ ] (by virtue of the above) At least one major reference implementation exists
-- [ ] A well established testing criteria (defined at the outset of the project) is met (testing via Testground or otherwise)
-- [ ] Public documentation (on docs.libp2p) exists
+The "Definition of Done" for projects/sub-projects that involve writing new protocols/ modify existing ones usually consist of the following:
+- If a specification change is required:
+    - [ ] Spec is merged and classified as "Candidate Recommendation"
+    - [ ] (by virtue of the above) At least one major reference implementation exists
+- [ ] A well established testing criteria is met (defined at the outset of the project including but not limited to testing via Testground, compatibility tests with other implementations in the Release process, etc.)
+- [ ] Public documentation (on docs.libp2p.io) exists
+
+Supporting projects (such as testing or benchmarking) may have different criteria.
+
+## Benchmarking and Testing
+As mentioned in our [vision](#vision), performance and stability are core libp2p tenets. Rigorous benchmarking and testing help us uphold them. Related projects are listed in the [libp2p/test-plans roadmap](https://github.com/libp2p/test-plans/blob/master/ROADMAP.md) and the [testground/testground roadmap](https://github.com/testground/testground/blob/master/ROADMAP.md).
+Our major priorities in Q4’22 and Q1’23 are:
+- [interoperability testing](https://github.com/libp2p/test-plans/issues/53) (across implementations & versions and between transports, muxers, & security protocols)
+- performance [benchmark go-libp2p using Testground](https://github.com/testground/testground/pull/1425) (create a benchmark suite to run in CI, create a public performance dashboard, [demonstrate libp2p is able to achieve performance on par with HTTP](https://github.com/libp2p/test-plans/issues/27))
 
-Others supporting projects (like testing or benchmarking) will have different criteria.
+These projects are parallel workstreams, weighed equally with roadmap items in this document. Some efforts like interoperability testing have a higher priority than implementation projects. The go-libp2p maintainers co-own these efforts with the js-libp2p, rust-libp2p, and Testground maintainers.
 
+[**Click here to see the shared Q4’22/Q1’23 testing and benchmarking priorities.**](https://github.com/libp2p/test-plans/blob/master/ROADMAP.md)
 
 ## 🛣️ Milestones
 ### 2022
 
 #### Early Q4 (October)
--   A.1⚡Handshakes at the Speed of Light - **[Early Muxer Negotiation](https://github.com/libp2p/specs/issues/426)**
-- E.1 🧪 Future-proof testing - **RTT test for Early Muxer Negotiation**
+- [B.1 ⚡ Early Muxer Negotiation](#1-early-muxer-negotiation)
 
 #### Mid Q4 (November)
--   F.1 ⏱ Future-proof Benchmarking - **[Using nix-builders](https://github.com/testground/testground/pull/1425)**
+- [***➡️ test-plans/Interop tests for all existing/developing libp2p transports***](https://github.com/libp2p/test-plans/blob/master/ROADMAP.md#2-interop-test-plans-for-all-existingdeveloping-libp2p-transports)
+- [***➡️ test-plans/Benchmarking using nix-builders***](https://github.com/libp2p/test-plans/blob/master/ROADMAP.md#1-benchmarking-using-nix-builders)
 
 #### End of Q4 (December)
--   B.1 🧠 Smart Dialing - **Happy Eyeballs**
--   C 📊 Comprehensive Metrics - **Swarm metrics**
--   D.1 📺 Universal Browser Connectivity - **[WebRTC Browser -> Server](https://github.com/libp2p/go-libp2p/pull/1655)**
+- [A.1 📺 WebRTC Browser -> Server](#1-webrtc-for-browser-to-server)
+- [C.1 🧠 Happy Eyeballs](#1-happy-eyeballs)
+- [D 📊 Swarm Metrics](#e--comprehensive-metrics)
 
 ### 2023
 
 #### Early Q1 (January)
--   B.2 🧠 Smart Dialing - **QUIC Blackhole detector**
--   A.2 ⚡ Handshakes at the Speed of Light - **[Adding security protocol](https://github.com/libp2p/specs/pull/353)**
+- [B.2 ⚡ Adding security protocol](#2-adding-security-protocol)
+- [C.2 🧠 QUIC Blackhole detector](#2-quic-blackhole-detector)
 
 #### Mid Q1 (February)
--   B.3 🧠 Smart Dialing - **RTT estimation**
-    -   🎉 Estimated Project Completion
+- [C.3 🧠 RTT estimation](#3-rtt-estimation)
+  - 🎉 Estimated Project Completion
 
 #### End of Q1 (March)
--   F.2 ⏱️ Future-proof benchmarking - **Using first class support for remote runners in Testground**
--   A.3 ⚡ Handshakes at the Speed of Light - **0.5 RTT data optimization** (for QUIC)
-    -   🎉 Estimated Project Completion
+- [B.3 ⚡ 0.5 RTT data optimization (for QUIC)](#3-05-rtt-data-optimization)
+  -  🎉 Estimated Project Completion
+- [***➡️ test-plans/Benchmarking using remote runners***](https://github.com/libp2p/test-plans/blob/master/ROADMAP.md#2-benchmarking-using-remote-runners)
+
+### Up Next
+- [A.2 📺 WebRTC: Browser to Browser](#2-webrtc-browser-to-browser)
+- [A.3 📺 WebTransport: Update to new draft versions](#3-webtransport-update-to-new-draft-versions)
+- [***➡️ test-plans/Expansive protocol test coverage***](https://github.com/libp2p/test-plans/blob/master/ROADMAP.md#d-expansive-protocol-test-coverage)
+- [E 📢 Judicious Address Advertisements](#f--judicious-address-advertisements)
+
+## 📖 Appendix
+
+**Projects are listed in descending priority.**
+
+### [A. 📺 Universal Browser Connectivity](https://github.com/libp2p/go-libp2p/issues/1811)
+
+**Why**: A huge part of “the Web” is happening inside the browser. As a universal p2p networking stack, libp2p needs to be able to offer solutions for browser users.
+
+**Goal**: go-libp2p ships with up-to-date WebTransport and (libp2p-) WebRTC implementations, enabled by default. This allows connections between browsers and public nodes, browsers and non-public nodes, as well as two browsers.
 
-#### Up Next
--   D.2 📺 Universal Browser Connectivity - **[WebTransport](https://github.com/libp2p/go-libp2p/issues/1717)** update to new draft versions
-    - Dependency on Chrome needs to support new draft version of WebTransport protocol (still under development by IETF and W3C)
--  E.2 🧪 Future-proof testing - **Comprehensive testing for non-transport features**
--  G 📢 Judicious Address Advertisements
+#### 1. [WebRTC: Browser to Server](https://github.com/libp2p/go-libp2p/pull/1655)
+Add support for WebRTC transport in go-libp2p, enabling browser connectivity with servers. This will cover the browsers that don't support WebTransport (most notable is iOS Safari). This is getting close to finalized.
+#### 2. WebRTC: Browser to Browser
+A follow up to A.1 where we will begin the work to specify the semantics of browser to browser connectivity and then implement it in go-libp2p.
+#### 3. [WebTransport: Update to new draft versions](https://github.com/libp2p/go-libp2p/issues/1717)
+As the protocol is still under development by IETF and W3C, the go-libp2p implementation needs to follow. We have a dependency on Chrome to support the new draft version of WebTransport protocol. To stay up to date, we will have to move as soon as Chrome ships supports the new draft version.
 
-# Roadmap Appendix
-## [A.⚡️Handshakes at the Speed of Light](https://github.com/libp2p/go-libp2p/issues/1807)
+### [B. ⚡️ Handshakes at the Speed of Light](https://github.com/libp2p/go-libp2p/issues/1807)
 
 **Why**: Historically, libp2p has been very wasteful when it comes to round trips spent during connection establishment. This is slowing down our users, especially their TTFB (time to first byte) metrics.
 
 **Goal**: go-libp2p optimizes its handshake latency up to the point where only increasing the speed of light would lead to further speedups. In particular, this means:
 
-1. [Early Muxer Negotiation](https://github.com/libp2p/specs/issues/426): cutting off the 1 RTT wasted on muxer negotiation
-2. [Adding security protocol](https://github.com/libp2p/specs/pull/353): cutting off the 1 RTT wasted on security protocol negotiation by including the security protocol in the multiaddr
-3. 0.5 RTT data optimization: using 0.5-RTT data (for TLS) / a Noise Extension to ship the list of Identify protocols, cutting of 1 RTT that many protocols spend waiting on `IdentifyWait`
+#### 1. [Early Muxer Negotiation](https://github.com/libp2p/specs/issues/426)
+Cutting off the 1 RTT wasted on muxer negotiation
+#### 2. [Adding security protocol](https://github.com/libp2p/specs/pull/353)
+Cutting off the 1 RTT wasted on security protocol negotiation by including the security protocol in the multiaddr
+#### 3. 0.5 RTT data optimization
+Using 0.5-RTT data (for TLS) / a Noise Extension to ship the list of Identify protocols, cutting of 1 RTT that many protocols spend waiting on `IdentifyWait`
 
-## [B. 🧠 Smart Dialing](https://github.com/libp2p/go-libp2p/issues/1808)
+### [C. 🧠 Smart Dialing](https://github.com/libp2p/go-libp2p/issues/1808)
 
 **Why**: Having a large list of transports to pick from is great. Having an advanced stack that can dial all of them is even greater. But dialing all of them at the same time wastes our, the network’s and the peer’s resources. 
 
 **Goal**: When given a list of multiaddrs of a peer, go-libp2p is smart enough to pick the address that results in the most performant connection (for example, preferring QUIC over TCP), while also picking the address such that maximizes the likelihood of a successful handshake.
 
-How:
+#### 1. [Happy Eyeballs](https://github.com/libp2p/go-libp2p/issues/1785)
+Implement some kind of “Happy-Eyeballs” style prioritization among all supported transports
+#### 2. QUIC Blackhole detector
+Detection of blackholes, especially relevant to detect UDP (QUIC) blackholing
+#### 3. RTT estimation
+Estimation of the expected RTT of a connection based on two nodes’ IP addresses, so that Happy Eyeballs Timeouts can be set dynamically
 
-1. Happy Eyeballs: implement some kind of “Happy-Eyeballs” style prioritization among all supported transports
-2. QUIC Blackhole detector: detection of blackholes, especially relevant to detect UDP (QUIC) blackholing
-3. RTT estimation: estimation of the expected RTT of a connection based on two nodes’ IP addresses, so that Happy Eyeballs Timeouts can be set dynamically
-
-## [C. 📊 Comprehensive Metrics](https://github.com/libp2p/go-libp2p/issues/1356)
+### [D. 📊 Comprehensive Metrics](https://github.com/libp2p/go-libp2p/issues/1356)
 
 **Why**: For far too long, go-libp2p has been a black box. This has hurt us many times, by allowing trivial bugs to go undetected for a long time ([example](https://github.com/ipfs/kubo/pull/8750)). Having metrics will allow us to track the impact of performance improvements we make over time.
 
@@ -97,35 +166,8 @@ How:
 
 **How**: This will look similar to how we already expose resource manager metrics. Metrics can be added incrementally for libp2p’s components. First milestone is having metrics for the swarm.
 
-## [D. 📺 Universal Browser Connectivity](https://github.com/libp2p/go-libp2p/issues/1811)
-
-**Why**: A huge part of “the Web” is happening inside the browser. As a universal p2p networking stack, libp2p needs to be able to offer solutions for browser users.
-
-**Goal**: go-libp2p ships with up-to-date WebTransport and (libp2p-) WebRTC implementations, enabled by default. This allows connections between browsers and public nodes, browsers and non-public nodes as well as two browsers.
-
-1. [WebRTC](https://github.com/libp2p/go-libp2p/pull/1655): while browser to public node is getting close to finalized, there’ll be a push to make the other combinations work as well
-2. [WebTransport](https://github.com/libp2p/go-libp2p/issues/1717): as the protocol is still under development by IETF and W3C, our implementation needs to follow. To stay up to date, we will have to move as soon as Chrome ships support for a new draft version
-
-## [E. 🧪 Future-proof Testing](https://github.com/libp2p/go-libp2p/issues/1809)
-
-**Why**: Having lots of transports, encryption mechanism, and stream muxers is great, and shaving off RTTs is awesome. We need to stay backwards-compatible with legacy go-libp2p release, other libp2p implementations, and less advanced libp2p stacks.
 
-**Goal**: We have cross-version and cross-implementation Testground coverage that makes sure that we are able to establish a libp2p connection between two nodes, in the expected number of RTTs. This makes sure that the optimizations don’t break compatibility, actually have the performance impact we expect them to have, and serves as a regression test in the future.
-
-1. RTT test for Early Muxer Negotiation (A.1)
-2. Comprehensive testing for non-transport features (AutoNat, Hole punching, etc.)
-
-## [F. ⏱ Future-proof Benchmarking](https://github.com/libp2p/go-libp2p/issues/1810)
-
-**Why**: For libp2p to be competitive, it needs to delivers comparable performance to widely used protocols on the internet, namely HTTP/2 and HTTP/3.
-
-**Goal**: We have a test suite that runs libp2p transfers between nodes located at different locations all over the world, proving that libp2p is able to achieve performance on par with HTTP. The test suite is run on a continuous basis and results are published to a public performance dashboard.
-
-1. [Benchmarking using nix-builders](https://github.com/testground/testground/pull/1425)
-    - (Dependency: remote machines need Nix installed)
-2. Benchmarking using first class support for remote runners (using `remote:exec`) in Testground
-
-## [G. 📢 Judicious Address Advertisements](https://github.com/libp2p/go-libp2p/issues/1812)
+### [E. 📢 Judicious Address Advertisements](https://github.com/libp2p/go-libp2p/issues/1812)
 
 **Why**: A node that advertises lots of addresses hurts itself. Other nodes will have to try dialing a lot of addresses before they find one that actually works, dramatically increasing handshake latencies.
 
@@ -134,6 +176,3 @@ How:
 **How**: Unfortunately, the AutoNAT protocol can’t be used to probe the reachability of any particular address (especially due to a bug in the go-libp2p implementation deployed years ago). Most likely, we need a second version of the AutoNAT protocol.
 
 Related discussion: [https://github.com/libp2p/go-libp2p/issues/1480](https://github.com/libp2p/go-libp2p/issues/1480)
-
-## Libp2p Project Roadmap
-Roadmap items in this document were sourced from our the overarching libp2p project roadmap: https://github.com/libp2p/specs/blob/master/ROADMAP.md
diff --git a/core/network/rcmgr.go b/core/network/rcmgr.go
index b6092575..524a28a8 100644
--- a/core/network/rcmgr.go
+++ b/core/network/rcmgr.go
@@ -271,60 +271,57 @@ type ScopeStat struct {
 }
 
 // NullResourceManager is a stub for tests and initialization of default values
-var NullResourceManager ResourceManager = &nullResourceManager{}
-
-type nullResourceManager struct{}
-type nullScope struct{}
-
-var _ ResourceScope = (*nullScope)(nil)
-var _ ResourceScopeSpan = (*nullScope)(nil)
-var _ ServiceScope = (*nullScope)(nil)
-var _ ProtocolScope = (*nullScope)(nil)
-var _ PeerScope = (*nullScope)(nil)
-var _ ConnManagementScope = (*nullScope)(nil)
-var _ ConnScope = (*nullScope)(nil)
-var _ StreamManagementScope = (*nullScope)(nil)
-var _ StreamScope = (*nullScope)(nil)
+type NullResourceManager struct{}
+
+var _ ResourceScope = (*NullScope)(nil)
+var _ ResourceScopeSpan = (*NullScope)(nil)
+var _ ServiceScope = (*NullScope)(nil)
+var _ ProtocolScope = (*NullScope)(nil)
+var _ PeerScope = (*NullScope)(nil)
+var _ ConnManagementScope = (*NullScope)(nil)
+var _ ConnScope = (*NullScope)(nil)
+var _ StreamManagementScope = (*NullScope)(nil)
+var _ StreamScope = (*NullScope)(nil)
 
 // NullScope is a stub for tests and initialization of default values
-var NullScope = &nullScope{}
+type NullScope struct{}
 
-func (n *nullResourceManager) ViewSystem(f func(ResourceScope) error) error {
-	return f(NullScope)
+func (n *NullResourceManager) ViewSystem(f func(ResourceScope) error) error {
+	return f(&NullScope{})
 }
-func (n *nullResourceManager) ViewTransient(f func(ResourceScope) error) error {
-	return f(NullScope)
+func (n *NullResourceManager) ViewTransient(f func(ResourceScope) error) error {
+	return f(&NullScope{})
 }
-func (n *nullResourceManager) ViewService(svc string, f func(ServiceScope) error) error {
-	return f(NullScope)
+func (n *NullResourceManager) ViewService(svc string, f func(ServiceScope) error) error {
+	return f(&NullScope{})
 }
-func (n *nullResourceManager) ViewProtocol(p protocol.ID, f func(ProtocolScope) error) error {
-	return f(NullScope)
+func (n *NullResourceManager) ViewProtocol(p protocol.ID, f func(ProtocolScope) error) error {
+	return f(&NullScope{})
 }
-func (n *nullResourceManager) ViewPeer(p peer.ID, f func(PeerScope) error) error {
-	return f(NullScope)
+func (n *NullResourceManager) ViewPeer(p peer.ID, f func(PeerScope) error) error {
+	return f(&NullScope{})
 }
-func (n *nullResourceManager) OpenConnection(dir Direction, usefd bool, endpoint multiaddr.Multiaddr) (ConnManagementScope, error) {
-	return NullScope, nil
+func (n *NullResourceManager) OpenConnection(dir Direction, usefd bool, endpoint multiaddr.Multiaddr) (ConnManagementScope, error) {
+	return &NullScope{}, nil
 }
-func (n *nullResourceManager) OpenStream(p peer.ID, dir Direction) (StreamManagementScope, error) {
-	return NullScope, nil
+func (n *NullResourceManager) OpenStream(p peer.ID, dir Direction) (StreamManagementScope, error) {
+	return &NullScope{}, nil
 }
-func (n *nullResourceManager) Close() error {
+func (n *NullResourceManager) Close() error {
 	return nil
 }
 
-func (n *nullScope) ReserveMemory(size int, prio uint8) error { return nil }
-func (n *nullScope) ReleaseMemory(size int)                   {}
-func (n *nullScope) Stat() ScopeStat                          { return ScopeStat{} }
-func (n *nullScope) BeginSpan() (ResourceScopeSpan, error)    { return NullScope, nil }
-func (n *nullScope) Done()                                    {}
-func (n *nullScope) Name() string                             { return "" }
-func (n *nullScope) Protocol() protocol.ID                    { return "" }
-func (n *nullScope) Peer() peer.ID                            { return "" }
-func (n *nullScope) PeerScope() PeerScope                     { return NullScope }
-func (n *nullScope) SetPeer(peer.ID) error                    { return nil }
-func (n *nullScope) ProtocolScope() ProtocolScope             { return NullScope }
-func (n *nullScope) SetProtocol(proto protocol.ID) error      { return nil }
-func (n *nullScope) ServiceScope() ServiceScope               { return NullScope }
-func (n *nullScope) SetService(srv string) error              { return nil }
+func (n *NullScope) ReserveMemory(size int, prio uint8) error { return nil }
+func (n *NullScope) ReleaseMemory(size int)                   {}
+func (n *NullScope) Stat() ScopeStat                          { return ScopeStat{} }
+func (n *NullScope) BeginSpan() (ResourceScopeSpan, error)    { return &NullScope{}, nil }
+func (n *NullScope) Done()                                    {}
+func (n *NullScope) Name() string                             { return "" }
+func (n *NullScope) Protocol() protocol.ID                    { return "" }
+func (n *NullScope) Peer() peer.ID                            { return "" }
+func (n *NullScope) PeerScope() PeerScope                     { return &NullScope{} }
+func (n *NullScope) SetPeer(peer.ID) error                    { return nil }
+func (n *NullScope) ProtocolScope() ProtocolScope             { return &NullScope{} }
+func (n *NullScope) SetProtocol(proto protocol.ID) error      { return nil }
+func (n *NullScope) ServiceScope() ServiceScope               { return &NullScope{} }
+func (n *NullScope) SetService(srv string) error              { return nil }
diff --git a/go.mod b/go.mod
index 80dc32bc..3c6128f8 100644
--- a/go.mod
+++ b/go.mod
@@ -31,9 +31,9 @@ require (
 	github.com/libp2p/go-reuseport v0.2.0
 	github.com/libp2p/go-yamux/v4 v4.0.0
 	github.com/libp2p/zeroconf/v2 v2.2.0
-	github.com/lucas-clemente/quic-go v0.29.1
+	github.com/lucas-clemente/quic-go v0.30.0
 	github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd
-	github.com/marten-seemann/webtransport-go v0.1.1
+	github.com/marten-seemann/webtransport-go v0.2.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
@@ -76,6 +76,7 @@ require (
 	github.com/godbus/dbus/v5 v5.1.0 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
+	github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
 	github.com/google/uuid v1.3.0 // indirect
 	github.com/huin/goupnp v1.0.3 // indirect
 	github.com/jackpal/go-nat-pmp v1.0.2 // indirect
@@ -83,17 +84,17 @@ require (
 	github.com/klauspost/cpuid/v2 v2.1.1 // indirect
 	github.com/koron/go-ssdp v0.0.3 // indirect
 	github.com/libp2p/go-cidranger v1.1.0 // 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/marten-seemann/qpack v0.3.0 // indirect
+	github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect
+	github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect
 	github.com/mattn/go-isatty v0.0.16 // indirect
 	github.com/mattn/go-pointer v0.0.1 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
 	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/nxadm/tail v1.4.8 // indirect
 	github.com/onsi/ginkgo v1.16.5 // indirect
+	github.com/onsi/ginkgo/v2 v2.2.0 // indirect
 	github.com/opencontainers/runtime-spec v1.0.2 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -112,7 +113,6 @@ require (
 	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
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	lukechampine.com/blake3 v1.1.7 // indirect
 )
diff --git a/go.sum b/go.sum
index 35541b03..c9161da5 100644
--- a/go.sum
+++ b/go.sum
@@ -208,6 +208,8 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -233,6 +235,7 @@ github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ=
 github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y=
 github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/ipfs/go-cid v0.3.2 h1:OGgOd+JCFM+y1DjWPmVH+2/4POtpDzwcr7VgnB7mZXc=
 github.com/ipfs/go-cid v0.3.2/go.mod h1:gQ8pKqT/sUxGY+tIwy1RPpAojYu7jAyCp5Tz1svoupw=
@@ -317,21 +320,21 @@ github.com/libp2p/go-yamux/v4 v4.0.0 h1:+Y80dV2Yx/kv7Y7JKu0LECyVdMXm1VUoko+VQ9rB
 github.com/libp2p/go-yamux/v4 v4.0.0/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4=
 github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q=
 github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
-github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmWanXB8zP0=
-github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
+github.com/lucas-clemente/quic-go v0.30.0 h1:nwLW0h8ahVQ5EPTIM7uhl/stHqQDea15oRlYKZmw2O0=
+github.com/lucas-clemente/quic-go v0.30.0/go.mod h1:ssOrRsOmdxa768Wr78vnh2B8JozgLsMzG/g+0qEC7uk=
 github.com/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-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 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
-github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
+github.com/marten-seemann/qpack v0.3.0 h1:UiWstOgT8+znlkDPOg2+3rIuYXJ2CnGDkGUXN6ki6hE=
+github.com/marten-seemann/qpack v0.3.0/go.mod h1:cGfKPBiP4a9EQdxCwEwI/GEeWAsjSekBvx/X8mh58+g=
+github.com/marten-seemann/qtls-go1-18 v0.1.3 h1:R4H2Ks8P6pAtUagjFty2p7BVHn3XiwDAl7TTQf5h7TI=
+github.com/marten-seemann/qtls-go1-18 v0.1.3/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
+github.com/marten-seemann/qtls-go1-19 v0.1.1 h1:mnbxeq3oEyQxQXwI4ReCgW9DPoPR94sNlqWoDZnjRIE=
+github.com/marten-seemann/qtls-go1-19 v0.1.1/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.1 h1:TnyKp3pEXcDooTaNn4s9dYpMJ7kMnTp7k5h+SgYP/mc=
-github.com/marten-seemann/webtransport-go v0.1.1/go.mod h1:kBEh5+RSvOA4troP1vyOVBWK4MIMzDICXVrvCPrYcrM=
+github.com/marten-seemann/webtransport-go v0.2.0 h1:987jPVqcyE3vF+CHNIxDhT0P21O+bI4fVF+0NoRujSo=
+github.com/marten-seemann/webtransport-go v0.2.0/go.mod h1:XmnWYsWXaxUF7kjeIIzLWPyS+q0OcBY5vA64NuyK0ps=
 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=
@@ -398,13 +401,14 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 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.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
+github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
 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.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
 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=
@@ -702,7 +706,6 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -904,7 +907,6 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/p2p/net/mock/mock_conn.go b/p2p/net/mock/mock_conn.go
index 48015a4c..487db8c7 100644
--- a/p2p/net/mock/mock_conn.go
+++ b/p2p/net/mock/mock_conn.go
@@ -189,5 +189,5 @@ func (c *conn) Stat() network.ConnStats {
 }
 
 func (c *conn) Scope() network.ConnScope {
-	return network.NullScope
+	return &network.NullScope{}
 }
diff --git a/p2p/net/mock/mock_peernet.go b/p2p/net/mock/mock_peernet.go
index 2e931a66..3fb07013 100644
--- a/p2p/net/mock/mock_peernet.go
+++ b/p2p/net/mock/mock_peernet.go
@@ -370,5 +370,5 @@ func (pn *peernet) notifyAll(notification func(f network.Notifiee)) {
 }
 
 func (pn *peernet) ResourceManager() network.ResourceManager {
-	return network.NullResourceManager
+	return &network.NullResourceManager{}
 }
diff --git a/p2p/net/mock/mock_stream.go b/p2p/net/mock/mock_stream.go
index 61303dab..a78329d3 100644
--- a/p2p/net/mock/mock_stream.go
+++ b/p2p/net/mock/mock_stream.go
@@ -287,7 +287,7 @@ func (s *stream) transport() {
 }
 
 func (s *stream) Scope() network.StreamScope {
-	return network.NullScope
+	return &network.NullScope{}
 }
 
 func (s *stream) cancelWrite(err error) {
diff --git a/p2p/net/swarm/swarm.go b/p2p/net/swarm/swarm.go
index ece0986d..213f0fc6 100644
--- a/p2p/net/swarm/swarm.go
+++ b/p2p/net/swarm/swarm.go
@@ -178,7 +178,7 @@ func NewSwarm(local peer.ID, peers peerstore.Peerstore, opts ...Option) (*Swarm,
 		}
 	}
 	if s.rcmgr == nil {
-		s.rcmgr = network.NullResourceManager
+		s.rcmgr = &network.NullResourceManager{}
 	}
 
 	s.dsync = newDialSync(s.dialWorkerLoop)
diff --git a/p2p/net/upgrader/upgrader.go b/p2p/net/upgrader/upgrader.go
index 2ef60b82..ea19d2f2 100644
--- a/p2p/net/upgrader/upgrader.go
+++ b/p2p/net/upgrader/upgrader.go
@@ -90,7 +90,7 @@ func New(secureMuxer sec.SecureMuxer, muxer network.Multiplexer, opts ...Option)
 		}
 	}
 	if u.rcmgr == nil {
-		u.rcmgr = network.NullResourceManager
+		u.rcmgr = &network.NullResourceManager{}
 	}
 	return u, nil
 }
diff --git a/p2p/protocol/internal/circuitv1-deprecated/dial.go b/p2p/protocol/internal/circuitv1-deprecated/dial.go
index acc1217c..23c03b57 100644
--- a/p2p/protocol/internal/circuitv1-deprecated/dial.go
+++ b/p2p/protocol/internal/circuitv1-deprecated/dial.go
@@ -17,7 +17,7 @@ func (d *RelayTransport) Dial(ctx context.Context, a ma.Multiaddr, p peer.ID) (t
 		return nil, err
 	}
 	c.tagHop()
-	scope, _ := network.NullResourceManager.OpenConnection(network.DirOutbound, false, a)
+	scope, _ := (&network.NullResourceManager{}).OpenConnection(network.DirOutbound, false, a)
 	return d.upgrader.Upgrade(ctx, d, c, network.DirOutbound, p, scope)
 }
 
diff --git a/p2p/security/noise/pb/payload.pb.go b/p2p/security/noise/pb/payload.pb.go
index 84db783e..fdebe487 100644
--- a/p2p/security/noise/pb/payload.pb.go
+++ b/p2p/security/noise/pb/payload.pb.go
@@ -24,6 +24,7 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
 
 type NoiseExtensions struct {
 	WebtransportCerthashes [][]byte `protobuf:"bytes,1,rep,name=webtransport_certhashes,json=webtransportCerthashes" json:"webtransport_certhashes,omitempty"`
+	StreamMuxers           []string `protobuf:"bytes,2,rep,name=stream_muxers,json=streamMuxers" json:"stream_muxers,omitempty"`
 }
 
 func (m *NoiseExtensions) Reset()         { *m = NoiseExtensions{} }
@@ -66,6 +67,13 @@ func (m *NoiseExtensions) GetWebtransportCerthashes() [][]byte {
 	return nil
 }
 
+func (m *NoiseExtensions) GetStreamMuxers() []string {
+	if m != nil {
+		return m.StreamMuxers
+	}
+	return nil
+}
+
 type NoiseHandshakePayload struct {
 	IdentityKey []byte           `protobuf:"bytes,1,opt,name=identity_key,json=identityKey" json:"identity_key"`
 	IdentitySig []byte           `protobuf:"bytes,2,opt,name=identity_sig,json=identitySig" json:"identity_sig"`
@@ -134,21 +142,23 @@ func init() {
 func init() { proto.RegisterFile("payload.proto", fileDescriptor_678c914f1bee6d56) }
 
 var fileDescriptor_678c914f1bee6d56 = []byte{
-	// 221 bytes of a gzipped FileDescriptorProto
+	// 251 bytes of a gzipped FileDescriptorProto
 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2d, 0x48, 0xac, 0xcc,
-	0xc9, 0x4f, 0x4c, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x2a, 0x48, 0x52, 0xf2, 0xe2,
+	0xc9, 0x4f, 0x4c, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x2a, 0x48, 0x52, 0xca, 0xe7,
 	0xe2, 0xf7, 0xcb, 0xcf, 0x2c, 0x4e, 0x75, 0xad, 0x28, 0x49, 0xcd, 0x2b, 0xce, 0xcc, 0xcf, 0x2b,
 	0x16, 0x32, 0xe7, 0x12, 0x2f, 0x4f, 0x4d, 0x2a, 0x29, 0x4a, 0xcc, 0x2b, 0x2e, 0xc8, 0x2f, 0x2a,
 	0x89, 0x4f, 0x4e, 0x2d, 0x2a, 0xc9, 0x48, 0x2c, 0xce, 0x48, 0x2d, 0x96, 0x60, 0x54, 0x60, 0xd6,
-	0xe0, 0x09, 0x12, 0x43, 0x96, 0x76, 0x86, 0xcb, 0x2a, 0xcd, 0x63, 0xe4, 0x12, 0x05, 0x1b, 0xe6,
-	0x91, 0x98, 0x97, 0x52, 0x9c, 0x91, 0x98, 0x9d, 0x1a, 0x00, 0xb1, 0x4f, 0x48, 0x9d, 0x8b, 0x27,
-	0x33, 0x25, 0x35, 0xaf, 0x24, 0xb3, 0xa4, 0x32, 0x3e, 0x3b, 0xb5, 0x52, 0x82, 0x51, 0x81, 0x51,
-	0x83, 0xc7, 0x89, 0xe5, 0xc4, 0x3d, 0x79, 0x86, 0x20, 0x6e, 0x98, 0x8c, 0x77, 0x6a, 0x25, 0x8a,
-	0xc2, 0xe2, 0xcc, 0x74, 0x09, 0x26, 0x6c, 0x0a, 0x83, 0x33, 0xd3, 0x85, 0x8c, 0xb9, 0xb8, 0x52,
-	0xe1, 0x4e, 0x96, 0x60, 0x51, 0x60, 0xd4, 0xe0, 0x36, 0x12, 0xd6, 0x2b, 0x48, 0xd2, 0x43, 0xf3,
-	0x4d, 0x10, 0x92, 0x32, 0x27, 0x89, 0x13, 0x8f, 0xe4, 0x18, 0x2f, 0x3c, 0x92, 0x63, 0x7c, 0xf0,
-	0x48, 0x8e, 0x71, 0xc2, 0x63, 0x39, 0x86, 0x0b, 0x8f, 0xe5, 0x18, 0x6e, 0x3c, 0x96, 0x63, 0x00,
-	0x04, 0x00, 0x00, 0xff, 0xff, 0xb2, 0xb0, 0x39, 0x45, 0x1a, 0x01, 0x00, 0x00,
+	0xe0, 0x09, 0x12, 0x43, 0x96, 0x76, 0x86, 0xcb, 0x0a, 0x29, 0x73, 0xf1, 0x16, 0x97, 0x14, 0xa5,
+	0x26, 0xe6, 0xc6, 0xe7, 0x96, 0x56, 0xa4, 0x16, 0x15, 0x4b, 0x30, 0x29, 0x30, 0x6b, 0x70, 0x06,
+	0xf1, 0x40, 0x04, 0x7d, 0xc1, 0x62, 0x4a, 0xf3, 0x18, 0xb9, 0x44, 0xc1, 0x36, 0x7a, 0x24, 0xe6,
+	0xa5, 0x14, 0x67, 0x24, 0x66, 0xa7, 0x06, 0x40, 0x1c, 0x25, 0xa4, 0xce, 0xc5, 0x93, 0x99, 0x92,
+	0x9a, 0x57, 0x92, 0x59, 0x52, 0x19, 0x9f, 0x9d, 0x5a, 0x29, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0xe3,
+	0xc4, 0x72, 0xe2, 0x9e, 0x3c, 0x43, 0x10, 0x37, 0x4c, 0xc6, 0x3b, 0xb5, 0x12, 0x45, 0x61, 0x71,
+	0x66, 0xba, 0x04, 0x13, 0x36, 0x85, 0xc1, 0x99, 0xe9, 0x42, 0xc6, 0x5c, 0x5c, 0xa9, 0x70, 0x7f,
+	0x49, 0xb0, 0x28, 0x30, 0x6a, 0x70, 0x1b, 0x09, 0xeb, 0x15, 0x24, 0xe9, 0xa1, 0x79, 0x39, 0x08,
+	0x49, 0x99, 0x93, 0xc4, 0x89, 0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7,
+	0x38, 0xe1, 0xb1, 0x1c, 0xc3, 0x85, 0xc7, 0x72, 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x00, 0x02, 0x00,
+	0x00, 0xff, 0xff, 0x02, 0xdb, 0x23, 0xb3, 0x3f, 0x01, 0x00, 0x00,
 }
 
 func (m *NoiseExtensions) Marshal() (dAtA []byte, err error) {
@@ -171,6 +181,15 @@ func (m *NoiseExtensions) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	if len(m.StreamMuxers) > 0 {
+		for iNdEx := len(m.StreamMuxers) - 1; iNdEx >= 0; iNdEx-- {
+			i -= len(m.StreamMuxers[iNdEx])
+			copy(dAtA[i:], m.StreamMuxers[iNdEx])
+			i = encodeVarintPayload(dAtA, i, uint64(len(m.StreamMuxers[iNdEx])))
+			i--
+			dAtA[i] = 0x12
+		}
+	}
 	if len(m.WebtransportCerthashes) > 0 {
 		for iNdEx := len(m.WebtransportCerthashes) - 1; iNdEx >= 0; iNdEx-- {
 			i -= len(m.WebtransportCerthashes[iNdEx])
@@ -255,6 +274,12 @@ func (m *NoiseExtensions) Size() (n int) {
 			n += 1 + l + sovPayload(uint64(l))
 		}
 	}
+	if len(m.StreamMuxers) > 0 {
+		for _, s := range m.StreamMuxers {
+			l = len(s)
+			n += 1 + l + sovPayload(uint64(l))
+		}
+	}
 	return n
 }
 
@@ -346,6 +371,38 @@ func (m *NoiseExtensions) Unmarshal(dAtA []byte) error {
 			m.WebtransportCerthashes = append(m.WebtransportCerthashes, make([]byte, postIndex-iNdEx))
 			copy(m.WebtransportCerthashes[len(m.WebtransportCerthashes)-1], dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field StreamMuxers", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowPayload
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthPayload
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthPayload
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.StreamMuxers = append(m.StreamMuxers, string(dAtA[iNdEx:postIndex]))
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipPayload(dAtA[iNdEx:])
diff --git a/p2p/security/noise/pb/payload.proto b/p2p/security/noise/pb/payload.proto
index 7c1b0bdc..ff303b0d 100644
--- a/p2p/security/noise/pb/payload.proto
+++ b/p2p/security/noise/pb/payload.proto
@@ -3,6 +3,7 @@ package pb;
 
 message NoiseExtensions {
 	repeated bytes webtransport_certhashes = 1;
+	repeated string stream_muxers = 2;
 }
 
 message NoiseHandshakePayload {
diff --git a/p2p/security/noise/session.go b/p2p/security/noise/session.go
index f1286b9f..ce8d97cd 100644
--- a/p2p/security/noise/session.go
+++ b/p2p/security/noise/session.go
@@ -40,6 +40,9 @@ type secureSession struct {
 	prologue []byte
 
 	initiatorEarlyDataHandler, responderEarlyDataHandler EarlyDataHandler
+
+	// ConnectionState holds state information releated to the secureSession entity.
+	connectionState network.ConnectionState
 }
 
 // newSecureSession creates a Noise session over the given insecureConn Conn, using
@@ -110,7 +113,7 @@ func (s *secureSession) RemotePublicKey() crypto.PubKey {
 }
 
 func (s *secureSession) ConnState() network.ConnectionState {
-	return network.ConnectionState{}
+	return s.connectionState
 }
 
 func (s *secureSession) SetDeadline(t time.Time) error {
@@ -128,3 +131,10 @@ func (s *secureSession) SetWriteDeadline(t time.Time) error {
 func (s *secureSession) Close() error {
 	return s.insecureConn.Close()
 }
+
+func SessionWithConnState(s *secureSession, muxer string) *secureSession {
+	if s != nil {
+		s.connectionState.NextProto = muxer
+	}
+	return s
+}
diff --git a/p2p/security/noise/transport.go b/p2p/security/noise/transport.go
index c6923698..7ae4d7de 100644
--- a/p2p/security/noise/transport.go
+++ b/p2p/security/noise/transport.go
@@ -7,13 +7,18 @@ import (
 	"github.com/libp2p/go-libp2p/core/canonicallog"
 	"github.com/libp2p/go-libp2p/core/crypto"
 	"github.com/libp2p/go-libp2p/core/peer"
+	"github.com/libp2p/go-libp2p/core/protocol"
 	"github.com/libp2p/go-libp2p/core/sec"
+	"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
 
 	manet "github.com/multiformats/go-multiaddr/net"
 )
 
 // ID is the protocol ID for noise
-const ID = "/noise"
+const (
+	ID          = "/noise"
+	maxProtoNum = 100
+)
 
 var _ sec.SecureTransport = &Transport{}
 
@@ -22,38 +27,51 @@ var _ sec.SecureTransport = &Transport{}
 type Transport struct {
 	localID    peer.ID
 	privateKey crypto.PrivKey
+	muxers     []string
 }
 
 // New creates a new Noise transport using the given private key as its
 // libp2p identity key.
-func New(privkey crypto.PrivKey) (*Transport, error) {
+func New(privkey crypto.PrivKey, muxers []protocol.ID) (*Transport, error) {
 	localID, err := peer.IDFromPrivateKey(privkey)
 	if err != nil {
 		return nil, err
 	}
 
+	smuxers := make([]string, 0, len(muxers))
+	for _, muxer := range muxers {
+		smuxers = append(smuxers, string(muxer))
+	}
+
 	return &Transport{
 		localID:    localID,
 		privateKey: privkey,
+		muxers:     smuxers,
 	}, nil
 }
 
 // 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, nil, nil, false)
+	responderEDH := newTransportEDH(t)
+	c, err := newSecureSession(t, ctx, insecure, p, nil, nil, responderEDH, false)
 	if err != nil {
 		addr, maErr := manet.FromNetAddr(insecure.RemoteAddr())
 		if maErr == nil {
 			canonicallog.LogPeerStatus(100, p, addr, "handshake_failure", "noise", "err", err.Error())
 		}
 	}
-	return c, err
+	return SessionWithConnState(c, responderEDH.MatchMuxers(false)), err
 }
 
 // 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, nil, nil, true)
+	initiatorEDH := newTransportEDH(t)
+	c, err := newSecureSession(t, ctx, insecure, p, nil, initiatorEDH, nil, true)
+	if err != nil {
+		return c, err
+	}
+	return SessionWithConnState(c, initiatorEDH.MatchMuxers(true)), err
 }
 
 func (t *Transport) WithSessionOptions(opts ...SessionOption) (sec.SecureTransport, error) {
@@ -65,3 +83,46 @@ func (t *Transport) WithSessionOptions(opts ...SessionOption) (sec.SecureTranspo
 	}
 	return st, nil
 }
+
+func matchMuxers(initiatorMuxers, responderMuxers []string) string {
+	for _, muxer := range responderMuxers {
+		for _, initMuxer := range initiatorMuxers {
+			if initMuxer == muxer {
+				return muxer
+			}
+		}
+	}
+	return ""
+}
+
+type transportEarlyDataHandler struct {
+	transport      *Transport
+	receivedMuxers []string
+}
+
+var _ EarlyDataHandler = &transportEarlyDataHandler{}
+
+func newTransportEDH(t *Transport) *transportEarlyDataHandler {
+	return &transportEarlyDataHandler{transport: t}
+}
+
+func (i *transportEarlyDataHandler) Send(context.Context, net.Conn, peer.ID) *pb.NoiseExtensions {
+	return &pb.NoiseExtensions{
+		StreamMuxers: i.transport.muxers,
+	}
+}
+
+func (i *transportEarlyDataHandler) Received(_ context.Context, _ net.Conn, extension *pb.NoiseExtensions) error {
+	// Discard messages with size or the number of protocols exceeding extension limit for security.
+	if extension != nil && len(extension.StreamMuxers) <= maxProtoNum {
+		i.receivedMuxers = extension.GetStreamMuxers()
+	}
+	return nil
+}
+
+func (i *transportEarlyDataHandler) MatchMuxers(isInitiator bool) string {
+	if isInitiator {
+		return matchMuxers(i.transport.muxers, i.receivedMuxers)
+	}
+	return matchMuxers(i.receivedMuxers, i.transport.muxers)
+}
diff --git a/p2p/transport/quic/tracer.go b/p2p/transport/internal/quicutils/tracer.go
similarity index 90%
rename from p2p/transport/quic/tracer.go
rename to p2p/transport/internal/quicutils/tracer.go
index 50f429dc..272b1be2 100644
--- a/p2p/transport/quic/tracer.go
+++ b/p2p/transport/internal/quicutils/tracer.go
@@ -1,4 +1,4 @@
-package libp2pquic
+package quicutils
 
 import (
 	"bufio"
@@ -7,17 +7,21 @@ import (
 	"os"
 	"time"
 
+	golog "github.com/ipfs/go-log/v2"
 	"github.com/klauspost/compress/zstd"
-
 	"github.com/lucas-clemente/quic-go/logging"
 	"github.com/lucas-clemente/quic-go/qlog"
 )
 
-var qlogTracer logging.Tracer
+var log = golog.Logger("quic-utils")
+
+// QLOGTracer holds a qlog tracer, if qlogging is enabled (enabled using the QLOGDIR environment variable).
+// Otherwise it is nil.
+var QLOGTracer logging.Tracer
 
 func init() {
 	if qlogDir := os.Getenv("QLOGDIR"); len(qlogDir) > 0 {
-		qlogTracer = initQlogger(qlogDir)
+		QLOGTracer = initQlogger(qlogDir)
 	}
 }
 
diff --git a/p2p/transport/quic/tracer_metrics.go b/p2p/transport/quic/tracer_metrics.go
index 624139fc..0ba48e0f 100644
--- a/p2p/transport/quic/tracer_metrics.go
+++ b/p2p/transport/quic/tracer_metrics.go
@@ -267,8 +267,8 @@ func (m *metricsConnTracer) SentPacket(hdr *logging.ExtendedHeader, size logging
 	sentPackets.WithLabelValues(m.getEncLevel(logging.PacketTypeFromHeader(&hdr.Header))).Inc()
 }
 
-func (m *metricsConnTracer) ReceivedVersionNegotiationPacket(hdr *logging.Header, v []logging.VersionNumber) {
-	bytesTransferred.WithLabelValues("rcvd").Add(float64(hdr.ParsedLen() + logging.ByteCount(4*len(v))))
+func (m *metricsConnTracer) ReceivedVersionNegotiationPacket(dst, src logging.ArbitraryLenConnectionID, v []logging.VersionNumber) {
+	bytesTransferred.WithLabelValues("rcvd").Add(1 /* header form byte */ + 4 /* version number */ + 2 /* src and dest conn id length fields */ + float64(dst.Len()+src.Len()) + float64(4*len(v)))
 	rcvdPackets.WithLabelValues("Version Negotiation").Inc()
 }
 
@@ -281,7 +281,7 @@ func (m *metricsConnTracer) ReceivedPacket(hdr *logging.ExtendedHeader, size log
 	rcvdPackets.WithLabelValues(m.getEncLevel(logging.PacketTypeFromHeader(&hdr.Header))).Inc()
 }
 
-func (m *metricsConnTracer) BufferedPacket(packetType logging.PacketType) {
+func (m *metricsConnTracer) BufferedPacket(packetType logging.PacketType, _ logging.ByteCount) {
 	bufferedPackets.WithLabelValues(m.getEncLevel(packetType)).Inc()
 }
 
diff --git a/p2p/transport/quic/transport.go b/p2p/transport/quic/transport.go
index 0b908db4..0a0af9fc 100644
--- a/p2p/transport/quic/transport.go
+++ b/p2p/transport/quic/transport.go
@@ -19,6 +19,7 @@ import (
 	"github.com/libp2p/go-libp2p/core/pnet"
 	tpt "github.com/libp2p/go-libp2p/core/transport"
 	p2ptls "github.com/libp2p/go-libp2p/p2p/security/tls"
+	"github.com/libp2p/go-libp2p/p2p/transport/internal/quicutils"
 
 	ma "github.com/multiformats/go-multiaddr"
 	mafmt "github.com/multiformats/go-multiaddr-fmt"
@@ -188,7 +189,7 @@ func NewTransport(key ic.PrivKey, psk pnet.PSK, gater connmgr.ConnectionGater, r
 		return nil, err
 	}
 	if rcmgr == nil {
-		rcmgr = network.NullResourceManager
+		rcmgr = &network.NullResourceManager{}
 	}
 	qconfig := quicConfig.Clone()
 	keyBytes, err := key.Raw()
@@ -201,7 +202,7 @@ func NewTransport(key ic.PrivKey, psk pnet.PSK, gater connmgr.ConnectionGater, r
 		return nil, err
 	}
 	var tracers []quiclogging.Tracer
-	if qlogTracer != nil {
+	if qlogTracer := quicutils.QLOGTracer; qlogTracer != nil {
 		tracers = append(tracers, qlogTracer)
 	}
 	if cfg.metrics {
diff --git a/p2p/transport/tcp/tcp.go b/p2p/transport/tcp/tcp.go
index 61bb38e2..b41fe7bf 100644
--- a/p2p/transport/tcp/tcp.go
+++ b/p2p/transport/tcp/tcp.go
@@ -136,7 +136,7 @@ var _ transport.Transport = &TcpTransport{}
 // created. It represents an entire TCP stack (though it might not necessarily be).
 func NewTCPTransport(upgrader transport.Upgrader, rcmgr network.ResourceManager, opts ...Option) (*TcpTransport, error) {
 	if rcmgr == nil {
-		rcmgr = network.NullResourceManager
+		rcmgr = &network.NullResourceManager{}
 	}
 	tr := &TcpTransport{
 		upgrader:       upgrader,
diff --git a/p2p/transport/websocket/addrs.go b/p2p/transport/websocket/addrs.go
index fed649dc..5fea8567 100644
--- a/p2p/transport/websocket/addrs.go
+++ b/p2p/transport/websocket/addrs.go
@@ -132,7 +132,7 @@ func parseMultiaddr(maddr ma.Multiaddr) (*url.URL, error) {
 
 type parsedWebsocketMultiaddr struct {
 	isWSS bool
-	// sni is the SNI value for the TLS handshake
+	// sni is the SNI value for the TLS handshake, and for setting HTTP Host header
 	sni *ma.Component
 	// the rest of the multiaddr before the /tls/sni/example.com/ws or /ws or /wss
 	restMultiaddr ma.Multiaddr
diff --git a/p2p/transport/websocket/websocket.go b/p2p/transport/websocket/websocket.go
index 2e9fc0b0..b693ef43 100644
--- a/p2p/transport/websocket/websocket.go
+++ b/p2p/transport/websocket/websocket.go
@@ -4,6 +4,7 @@ package websocket
 import (
 	"context"
 	"crypto/tls"
+	"net"
 	"net/http"
 	"time"
 
@@ -92,7 +93,7 @@ var _ transport.Transport = (*WebsocketTransport)(nil)
 
 func New(u transport.Upgrader, rcmgr network.ResourceManager, opts ...Option) (*WebsocketTransport, error) {
 	if rcmgr == nil {
-		rcmgr = network.NullResourceManager
+		rcmgr = &network.NullResourceManager{}
 	}
 	t := &WebsocketTransport{
 		upgrader:      u,
@@ -186,6 +187,17 @@ func (t *WebsocketTransport) maDial(ctx context.Context, raddr ma.Multiaddr) (ma
 			copytlsClientConf := t.tlsClientConf.Clone()
 			copytlsClientConf.ServerName = sni
 			dialer.TLSClientConfig = copytlsClientConf
+			ipAddr := wsurl.Host
+			// Setting the NetDial because we already have the resolved IP address, so we don't want to do another resolution.
+			// We set the `.Host` to the sni field so that the host header gets properly set.
+			dialer.NetDial = func(network, address string) (net.Conn, error) {
+				tcpAddr, err := net.ResolveTCPAddr(network, ipAddr)
+				if err != nil {
+					return nil, err
+				}
+				return net.DialTCP("tcp", nil, tcpAddr)
+			}
+			wsurl.Host = sni + ":" + wsurl.Port()
 		} else {
 			dialer.TLSClientConfig = t.tlsClientConf
 		}
diff --git a/p2p/transport/webtransport/conn.go b/p2p/transport/webtransport/conn.go
index 059258c7..d8a366a0 100644
--- a/p2p/transport/webtransport/conn.go
+++ b/p2p/transport/webtransport/conn.go
@@ -27,7 +27,7 @@ func (c *connMultiaddrs) RemoteMultiaddr() ma.Multiaddr { return c.remote }
 type conn struct {
 	*connSecurityMultiaddrs
 
-	transport tpt.Transport
+	transport *transport
 	session   *webtransport.Session
 
 	scope network.ConnScope
@@ -35,7 +35,7 @@ type conn struct {
 
 var _ tpt.CapableConn = &conn{}
 
-func newConn(tr tpt.Transport, sess *webtransport.Session, sconn *connSecurityMultiaddrs, scope network.ConnScope) *conn {
+func newConn(tr *transport, sess *webtransport.Session, sconn *connSecurityMultiaddrs, scope network.ConnScope) *conn {
 	return &conn{
 		connSecurityMultiaddrs: sconn,
 		transport:              tr,
@@ -60,7 +60,18 @@ func (c *conn) AcceptStream() (network.MuxedStream, error) {
 	return &stream{str}, nil
 }
 
-func (c *conn) Close() error             { return c.session.Close() }
+func (c *conn) allowWindowIncrease(size uint64) bool {
+	return c.scope.ReserveMemory(int(size), network.ReservationPriorityMedium) == nil
+}
+
+// Close closes the connection.
+// It must be called even if the peer closed the connection in order for
+// garbage collection to properly work in this package.
+func (c *conn) Close() error {
+	c.transport.removeConn(c.session)
+	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/listener.go b/p2p/transport/webtransport/listener.go
index 317afbe4..a5fae40d 100644
--- a/p2p/transport/webtransport/listener.go
+++ b/p2p/transport/webtransport/listener.go
@@ -9,12 +9,10 @@ import (
 	"net/http"
 	"time"
 
-	"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
-
-	"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"
+	"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
 
 	"github.com/lucas-clemente/quic-go/http3"
 	"github.com/marten-seemann/webtransport-go"
@@ -28,15 +26,10 @@ const queueLen = 16
 const handshakeTimeout = 10 * time.Second
 
 type listener struct {
-	transport       tpt.Transport
-	noise           *noise.Transport
-	certManager     *certManager
+	transport       *transport
 	tlsConf         *tls.Config
 	isStaticTLSConf bool
 
-	rcmgr network.ResourceManager
-	gater connmgr.ConnectionGater
-
 	server webtransport.Server
 
 	ctx       context.Context
@@ -52,7 +45,7 @@ type listener struct {
 
 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) {
+func newListener(laddr ma.Multiaddr, t *transport, tlsConf *tls.Config) (tpt.Listener, error) {
 	network, addr, err := manet.DialArgs(laddr)
 	if err != nil {
 		return nil, err
@@ -72,23 +65,22 @@ func newListener(laddr ma.Multiaddr, transport tpt.Transport, noise *noise.Trans
 	isStaticTLSConf := tlsConf != nil
 	if tlsConf == nil {
 		tlsConf = &tls.Config{GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
-			return certManager.GetConfig(), nil
+			return t.certManager.GetConfig(), nil
 		}}
 	}
 	ln := &listener{
-		transport:       transport,
-		noise:           noise,
-		certManager:     certManager,
+		transport:       t,
 		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},
+			H3: http3.Server{
+				QuicConfig: t.quicConfig,
+				TLSConfig:  tlsConf,
+			},
 			CheckOrigin: func(r *http.Request) bool { return true },
 		},
 	}
@@ -120,12 +112,12 @@ func (l *listener) httpHandler(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusBadRequest)
 		return
 	}
-	if l.gater != nil && !l.gater.InterceptAccept(&connMultiaddrs{local: l.multiaddr, remote: remoteMultiaddr}) {
+	if l.transport.gater != nil && !l.transport.gater.InterceptAccept(&connMultiaddrs{local: l.multiaddr, remote: remoteMultiaddr}) {
 		w.WriteHeader(http.StatusForbidden)
 		return
 	}
 
-	connScope, err := l.rcmgr.OpenConnection(network.DirInbound, false, remoteMultiaddr)
+	connScope, err := l.transport.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)
@@ -151,7 +143,7 @@ func (l *listener) httpHandler(w http.ResponseWriter, r *http.Request) {
 	}
 	cancel()
 
-	if l.gater != nil && !l.gater.InterceptSecured(network.DirInbound, sconn.RemotePeer(), sconn) {
+	if l.transport.gater != nil && !l.transport.gater.InterceptSecured(network.DirInbound, sconn.RemotePeer(), sconn) {
 		// TODO: can we close with a specific error here?
 		sess.Close()
 		connScope.Done()
@@ -165,8 +157,10 @@ func (l *listener) httpHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	conn := newConn(l.transport, sess, sconn, connScope)
+	l.transport.addConn(sess, conn)
 	select {
-	case l.queue <- newConn(l.transport, sess, sconn, connScope):
+	case l.queue <- conn:
 	default:
 		log.Debugw("accept queue full, dropping incoming connection", "peer", sconn.RemotePeer(), "addr", r.RemoteAddr, "error", err)
 		sess.Close()
@@ -199,10 +193,10 @@ func (l *listener) handshake(ctx context.Context, sess *webtransport.Session) (*
 	}
 	var earlyData [][]byte
 	if !l.isStaticTLSConf {
-		earlyData = l.certManager.SerializedCertHashes()
+		earlyData = l.transport.certManager.SerializedCertHashes()
 	}
 
-	n, err := l.noise.WithSessionOptions(noise.EarlyData(
+	n, err := l.transport.noise.WithSessionOptions(noise.EarlyData(
 		nil,
 		newEarlyDataSender(&pb.NoiseExtensions{WebtransportCerthashes: earlyData}),
 	))
@@ -225,10 +219,10 @@ func (l *listener) Addr() net.Addr {
 }
 
 func (l *listener) Multiaddr() ma.Multiaddr {
-	if l.certManager == nil {
+	if l.transport.certManager == nil {
 		return l.multiaddr
 	}
-	return l.multiaddr.Encapsulate(l.certManager.AddrComponent())
+	return l.multiaddr.Encapsulate(l.transport.certManager.AddrComponent())
 }
 
 func (l *listener) Close() error {
diff --git a/p2p/transport/webtransport/transport.go b/p2p/transport/webtransport/transport.go
index 5cd3a881..3d902b4e 100644
--- a/p2p/transport/webtransport/transport.go
+++ b/p2p/transport/webtransport/transport.go
@@ -11,16 +11,18 @@ import (
 	"sync"
 	"time"
 
-	"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
-
-	"github.com/benbjohnson/clock"
-	logging "github.com/ipfs/go-log/v2"
 	"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"
+	"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
+	"github.com/libp2p/go-libp2p/p2p/transport/internal/quicutils"
+
+	"github.com/benbjohnson/clock"
+	logging "github.com/ipfs/go-log/v2"
+	"github.com/lucas-clemente/quic-go"
 	"github.com/lucas-clemente/quic-go/http3"
 	"github.com/marten-seemann/webtransport-go"
 	ma "github.com/multiformats/go-multiaddr"
@@ -69,8 +71,9 @@ type transport struct {
 	pid     peer.ID
 	clock   clock.Clock
 
-	rcmgr network.ResourceManager
-	gater connmgr.ConnectionGater
+	quicConfig *quic.Config
+	rcmgr      network.ResourceManager
+	gater      connmgr.ConnectionGater
 
 	listenOnce    sync.Once
 	listenOnceErr error
@@ -79,6 +82,9 @@ type transport struct {
 	tlsClientConf *tls.Config
 
 	noise *noise.Transport
+
+	connMx sync.Mutex
+	conns  map[uint64]*conn // using quic-go's ConnectionTracingKey as map key
 }
 
 var _ tpt.Transport = &transport{}
@@ -96,17 +102,22 @@ func New(key ic.PrivKey, gater connmgr.ConnectionGater, rcmgr network.ResourceMa
 		rcmgr:   rcmgr,
 		gater:   gater,
 		clock:   clock.New(),
+		conns:   map[uint64]*conn{},
 	}
+	t.quicConfig = &quic.Config{AllowConnectionWindowIncrease: t.allowWindowIncrease}
 	for _, opt := range opts {
 		if err := opt(t); err != nil {
 			return nil, err
 		}
 	}
-	n, err := noise.New(key)
+	n, err := noise.New(key, nil)
 	if err != nil {
 		return nil, err
 	}
 	t.noise = n
+	if qlogTracer := quicutils.QLOGTracer; qlogTracer != nil {
+		t.quicConfig.Tracer = qlogTracer
+	}
 	return t, nil
 }
 
@@ -150,8 +161,9 @@ func (t *transport) Dial(ctx context.Context, raddr ma.Multiaddr, p peer.ID) (tp
 		scope.Done()
 		return nil, fmt.Errorf("secured connection gated")
 	}
-
-	return newConn(t, sess, sconn, scope), nil
+	conn := newConn(t, sess, sconn, scope)
+	t.addConn(sess, conn)
+	return conn, nil
 }
 
 func (t *transport) dial(ctx context.Context, addr string, sni string, certHashes []multihash.DecodedMultihash) (*webtransport.Session, error) {
@@ -176,7 +188,10 @@ func (t *transport) dial(ctx context.Context, addr string, sni string, certHashe
 		}
 	}
 	dialer := webtransport.Dialer{
-		RoundTripper: &http3.RoundTripper{TLSClientConfig: tlsConf},
+		RoundTripper: &http3.RoundTripper{
+			TLSClientConfig: tlsConf,
+			QuicConfig:      t.quicConfig,
+		},
 	}
 	rsp, sess, err := dialer.Dial(ctx, url, nil)
 	if err != nil {
@@ -284,7 +299,7 @@ func (t *transport) Listen(laddr ma.Multiaddr) (tpt.Listener, error) {
 			return nil, t.listenOnceErr
 		}
 	}
-	return newListener(laddr, t, t.noise, t.certManager, t.staticTLSConf, t.gater, t.rcmgr)
+	return newListener(laddr, t, t.staticTLSConf)
 }
 
 func (t *transport) Protocols() []int {
@@ -303,6 +318,29 @@ func (t *transport) Close() error {
 	return nil
 }
 
+func (t *transport) allowWindowIncrease(conn quic.Connection, size uint64) bool {
+	t.connMx.Lock()
+	defer t.connMx.Unlock()
+
+	c, ok := t.conns[conn.Context().Value(quic.ConnectionTracingKey).(uint64)]
+	if !ok {
+		return false
+	}
+	return c.allowWindowIncrease(size)
+}
+
+func (t *transport) addConn(sess *webtransport.Session, c *conn) {
+	t.connMx.Lock()
+	t.conns[sess.Context().Value(quic.ConnectionTracingKey).(uint64)] = c
+	t.connMx.Unlock()
+}
+
+func (t *transport) removeConn(sess *webtransport.Session) {
+	t.connMx.Lock()
+	delete(t.conns, sess.Context().Value(quic.ConnectionTracingKey).(uint64))
+	t.connMx.Unlock()
+}
+
 // extractSNI returns what the SNI should be for the given maddr. If there is an
 // SNI component in the multiaddr, then it will be returned and
 // foundSniComponent will be true. If there's no SNI component, but there is a


Test patch:
diff --git a/p2p/net/swarm/swarm_dial_test.go b/p2p/net/swarm/swarm_dial_test.go
index 959d51c2..e39a78dd 100644
--- a/p2p/net/swarm/swarm_dial_test.go
+++ b/p2p/net/swarm/swarm_dial_test.go
@@ -45,7 +45,7 @@ func TestAddrsForDial(t *testing.T) {
 	ps.AddPrivKey(id, priv)
 	t.Cleanup(func() { ps.Close() })
 
-	tpt, err := websocket.New(nil, network.NullResourceManager)
+	tpt, err := websocket.New(nil, &network.NullResourceManager{})
 	require.NoError(t, err)
 	s, err := NewSwarm(id, ps, WithMultiaddrResolver(resolver))
 	require.NoError(t, err)
@@ -81,7 +81,7 @@ func newTestSwarmWithResolver(t *testing.T, resolver *madns.Resolver) *Swarm {
 	})
 
 	// 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)
+	tpt, err := tcp.NewTCPTransport(nil, &network.NullResourceManager{})
 	require.NoError(t, err)
 	err = s.AddTransport(tpt)
 	require.NoError(t, err)
diff --git a/p2p/net/upgrader/listener_test.go b/p2p/net/upgrader/listener_test.go
index 82c3952e..fcb7bf9c 100644
--- a/p2p/net/upgrader/listener_test.go
+++ b/p2p/net/upgrader/listener_test.go
@@ -56,7 +56,7 @@ func TestAcceptSingleConn(t *testing.T) {
 	ln := createListener(t, u)
 	defer ln.Close()
 
-	cconn, err := dial(t, u, ln.Multiaddr(), id, network.NullScope)
+	cconn, err := dial(t, u, ln.Multiaddr(), id, &network.NullScope{})
 	require.NoError(err)
 
 	sconn, err := ln.Accept()
@@ -80,7 +80,7 @@ func TestAcceptMultipleConns(t *testing.T) {
 	}()
 
 	for i := 0; i < 10; i++ {
-		cconn, err := dial(t, u, ln.Multiaddr(), id, network.NullScope)
+		cconn, err := dial(t, u, ln.Multiaddr(), id, &network.NullScope{})
 		require.NoError(err)
 		toClose = append(toClose, cconn)
 
@@ -104,7 +104,7 @@ func TestConnectionsClosedIfNotAccepted(t *testing.T) {
 	ln := createListener(t, u)
 	defer ln.Close()
 
-	conn, err := dial(t, u, ln.Multiaddr(), id, network.NullScope)
+	conn, err := dial(t, u, ln.Multiaddr(), id, &network.NullScope{})
 	require.NoError(err)
 
 	errCh := make(chan error)
@@ -143,7 +143,7 @@ func TestFailedUpgradeOnListen(t *testing.T) {
 		errCh <- err
 	}()
 
-	_, err := dial(t, u, ln.Multiaddr(), id, network.NullScope)
+	_, err := dial(t, u, ln.Multiaddr(), id, &network.NullScope{})
 	require.Error(err)
 
 	// close the listener.
@@ -177,7 +177,7 @@ func TestListenerClose(t *testing.T) {
 	require.Contains(err.Error(), "use of closed network connection")
 
 	// doesn't accept new connections when it is closed
-	_, err = dial(t, u, ln.Multiaddr(), peer.ID("1"), network.NullScope)
+	_, err = dial(t, u, ln.Multiaddr(), peer.ID("1"), &network.NullScope{})
 	require.Error(err)
 }
 
@@ -189,7 +189,7 @@ func TestListenerCloseClosesQueued(t *testing.T) {
 
 	var conns []transport.CapableConn
 	for i := 0; i < 10; i++ {
-		conn, err := dial(t, upgrader, ln.Multiaddr(), id, network.NullScope)
+		conn, err := dial(t, upgrader, ln.Multiaddr(), id, &network.NullScope{})
 		require.NoError(err)
 		conns = append(conns, conn)
 	}
@@ -249,7 +249,7 @@ func TestConcurrentAccept(t *testing.T) {
 		go func() {
 			defer wg.Done()
 
-			conn, err := dial(t, u, ln.Multiaddr(), id, network.NullScope)
+			conn, err := dial(t, u, ln.Multiaddr(), id, &network.NullScope{})
 			if err != nil {
 				errCh <- err
 				return
@@ -279,7 +279,7 @@ func TestAcceptQueueBacklogged(t *testing.T) {
 	// setup AcceptQueueLength connections, but don't accept any of them
 	var counter int32 // to be used atomically
 	doDial := func() {
-		conn, err := dial(t, u, ln.Multiaddr(), id, network.NullScope)
+		conn, err := dial(t, u, ln.Multiaddr(), id, &network.NullScope{})
 		require.NoError(err)
 		atomic.AddInt32(&counter, 1)
 		t.Cleanup(func() { conn.Close() })
@@ -315,7 +315,7 @@ func TestListenerConnectionGater(t *testing.T) {
 	defer ln.Close()
 
 	// no gating.
-	conn, err := dial(t, u, ln.Multiaddr(), id, network.NullScope)
+	conn, err := dial(t, u, ln.Multiaddr(), id, &network.NullScope{})
 	require.NoError(err)
 	require.False(conn.IsClosed())
 	_ = conn.Close()
@@ -323,28 +323,28 @@ func TestListenerConnectionGater(t *testing.T) {
 	// rejecting after handshake.
 	testGater.BlockSecured(true)
 	testGater.BlockAccept(false)
-	conn, err = dial(t, u, ln.Multiaddr(), "invalid", network.NullScope)
+	conn, err = dial(t, u, ln.Multiaddr(), "invalid", &network.NullScope{})
 	require.Error(err)
 	require.Nil(conn)
 
 	// rejecting on accept will trigger firupgrader.
 	testGater.BlockSecured(true)
 	testGater.BlockAccept(true)
-	conn, err = dial(t, u, ln.Multiaddr(), "invalid", network.NullScope)
+	conn, err = dial(t, u, ln.Multiaddr(), "invalid", &network.NullScope{})
 	require.Error(err)
 	require.Nil(conn)
 
 	// rejecting only on acceptance.
 	testGater.BlockSecured(false)
 	testGater.BlockAccept(true)
-	conn, err = dial(t, u, ln.Multiaddr(), "invalid", network.NullScope)
+	conn, err = dial(t, u, ln.Multiaddr(), "invalid", &network.NullScope{})
 	require.Error(err)
 	require.Nil(conn)
 
 	// back to normal
 	testGater.BlockSecured(false)
 	testGater.BlockAccept(false)
-	conn, err = dial(t, u, ln.Multiaddr(), id, network.NullScope)
+	conn, err = dial(t, u, ln.Multiaddr(), id, &network.NullScope{})
 	require.NoError(err)
 	require.False(conn.IsClosed())
 	_ = conn.Close()
@@ -366,7 +366,7 @@ func TestListenerResourceManagement(t *testing.T) {
 		connScope.EXPECT().PeerScope(),
 	)
 
-	cconn, err := dial(t, upgrader, ln.Multiaddr(), id, network.NullScope)
+	cconn, err := dial(t, upgrader, ln.Multiaddr(), id, &network.NullScope{})
 	require.NoError(t, err)
 	defer cconn.Close()
 
@@ -384,7 +384,7 @@ func TestListenerResourceManagementDenied(t *testing.T) {
 	ln := createListener(t, upgrader)
 
 	rcmgr.EXPECT().OpenConnection(network.DirInbound, true, gomock.Not(ln.Multiaddr())).Return(nil, errors.New("nope"))
-	_, err := dial(t, upgrader, ln.Multiaddr(), id, network.NullScope)
+	_, err := dial(t, upgrader, ln.Multiaddr(), id, &network.NullScope{})
 	require.Error(t, err)
 
 	done := make(chan struct{})
diff --git a/p2p/net/upgrader/upgrader_test.go b/p2p/net/upgrader/upgrader_test.go
index 5d62e5f9..16188bd5 100644
--- a/p2p/net/upgrader/upgrader_test.go
+++ b/p2p/net/upgrader/upgrader_test.go
@@ -121,21 +121,21 @@ func TestOutboundConnectionGating(t *testing.T) {
 
 	testGater := &testGater{}
 	_, dialUpgrader := createUpgrader(t, upgrader.WithConnectionGater(testGater))
-	conn, err := dial(t, dialUpgrader, ln.Multiaddr(), id, network.NullScope)
+	conn, err := dial(t, dialUpgrader, ln.Multiaddr(), id, &network.NullScope{})
 	require.NoError(err)
 	require.NotNil(conn)
 	_ = conn.Close()
 
 	// blocking accepts doesn't affect the dialling side, only the listener.
 	testGater.BlockAccept(true)
-	conn, err = dial(t, dialUpgrader, ln.Multiaddr(), id, network.NullScope)
+	conn, err = dial(t, dialUpgrader, ln.Multiaddr(), id, &network.NullScope{})
 	require.NoError(err)
 	require.NotNil(conn)
 	_ = conn.Close()
 
 	// now let's block all connections after being secured.
 	testGater.BlockSecured(true)
-	conn, err = dial(t, dialUpgrader, ln.Multiaddr(), id, network.NullScope)
+	conn, err = dial(t, dialUpgrader, ln.Multiaddr(), id, &network.NullScope{})
 	require.Error(err)
 	require.Contains(err.Error(), "gater rejected connection")
 	require.Nil(conn)
@@ -153,7 +153,7 @@ func TestOutboundResourceManagement(t *testing.T) {
 		gomock.InOrder(
 			connScope.EXPECT().PeerScope(),
 			connScope.EXPECT().SetPeer(id),
-			connScope.EXPECT().PeerScope().Return(network.NullScope),
+			connScope.EXPECT().PeerScope().Return(&network.NullScope{}),
 		)
 		_, dialUpgrader := createUpgrader(t)
 		conn, err := dial(t, dialUpgrader, ln.Multiaddr(), id, connScope)
@@ -174,7 +174,7 @@ func TestOutboundResourceManagement(t *testing.T) {
 		gomock.InOrder(
 			connScope.EXPECT().PeerScope(),
 			connScope.EXPECT().SetPeer(id),
-			connScope.EXPECT().PeerScope().Return(network.NullScope),
+			connScope.EXPECT().PeerScope().Return(&network.NullScope{}),
 			connScope.EXPECT().Done(),
 		)
 		_, dialUpgrader := createUpgrader(t)
diff --git a/p2p/protocol/circuitv2/client/reservation_test.go b/p2p/protocol/circuitv2/client/reservation_test.go
index c50a4616..02a2f858 100644
--- a/p2p/protocol/circuitv2/client/reservation_test.go
+++ b/p2p/protocol/circuitv2/client/reservation_test.go
@@ -89,7 +89,7 @@ func TestReservationFailures(t *testing.T) {
 				host.SetStreamHandler(proto.ProtoIDv2Hop, tc.streamHandler)
 			}
 
-			cl, err := libp2p.New(libp2p.ResourceManager(network.NullResourceManager))
+			cl, err := libp2p.New(libp2p.ResourceManager(&network.NullResourceManager{}))
 			require.NoError(t, err)
 			defer cl.Close()
 			_, err = client.Reserve(context.Background(), cl, peer.AddrInfo{ID: host.ID(), Addrs: host.Addrs()})
diff --git a/p2p/security/noise/benchmark_test.go b/p2p/security/noise/benchmark_test.go
index 52454f59..836275b9 100644
--- a/p2p/security/noise/benchmark_test.go
+++ b/p2p/security/noise/benchmark_test.go
@@ -39,7 +39,7 @@ func makeTransport(b *testing.B) *Transport {
 	if err != nil {
 		b.Fatal(err)
 	}
-	tpt, err := New(priv)
+	tpt, err := New(priv, nil)
 	if err != nil {
 		b.Fatalf("error constructing transport: %v", err)
 	}
diff --git a/p2p/security/noise/transport_test.go b/p2p/security/noise/transport_test.go
index 2fa90d06..c3b18047 100644
--- a/p2p/security/noise/transport_test.go
+++ b/p2p/security/noise/transport_test.go
@@ -37,6 +37,12 @@ func newTestTransport(t *testing.T, typ, bits int) *Transport {
 	}
 }
 
+func newTestTransportWithMuxers(t *testing.T, typ, bits int, muxers []string) *Transport {
+	transport := newTestTransport(t, typ, bits)
+	transport.muxers = muxers
+	return transport
+}
+
 // Create a new pair of connected TCP sockets.
 func newConnPair(t *testing.T) (net.Conn, net.Conn) {
 	lstnr, err := net.Listen("tcp", "localhost:0")
@@ -586,3 +592,56 @@ func TestEarlyfffDataAcceptedWithNoHandler(t *testing.T) {
 		require.NoError(t, err)
 	}
 }
+
+type noiseEarlyDataTestCase struct {
+	initProtos     []string
+	respProtos     []string
+	expectedResult string
+}
+
+func TestHandshakeWithTransportEarlyData(t *testing.T) {
+	tests := []noiseEarlyDataTestCase{
+		{initProtos: nil, respProtos: nil, expectedResult: ""},
+		{[]string{"muxer1"}, []string{"muxer1"}, "muxer1"},
+		{[]string{"muxer1"}, []string{}, ""},
+		{[]string{}, []string{"muxer2"}, ""},
+		{[]string{"muxer2"}, []string{"muxer1"}, ""},
+		{[]string{"muxer1/1.0.0", "muxer2/1.0.1"}, []string{"muxer2/1.0.1", "muxer1/1.0.0"}, "muxer2/1.0.1"},
+		{[]string{"muxer1/1.0.0", "muxer2/1.0.1", "muxer3/1.0.0"}, []string{"muxer2/1.0.1", "muxer1/1.0.1", "muxer3/1.0.0"}, "muxer2/1.0.1"},
+		{[]string{"muxer1/1.0.0", "muxer2/1.0.0"}, []string{"muxer3/1.0.0"}, ""},
+	}
+
+	noiseHandshake := func(t *testing.T, initProtos, respProtos []string, expectedProto string) {
+		initTransport := newTestTransportWithMuxers(t, crypto.Ed25519, 2048, initProtos)
+		respTransport := newTestTransportWithMuxers(t, crypto.Ed25519, 2048, respProtos)
+
+		initConn, respConn := connect(t, initTransport, respTransport)
+		defer initConn.Close()
+		defer respConn.Close()
+
+		require.Equal(t, expectedProto, initConn.connectionState.NextProto)
+		require.Equal(t, expectedProto, respConn.connectionState.NextProto)
+
+		initData := []byte("Test data for noise transport")
+		_, err := initConn.Write(initData)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		respData := make([]byte, len(initData))
+		_, err = respConn.Read(respData)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if !bytes.Equal(initData, respData) {
+			t.Errorf("Data transmitted mismatch over noise session. %v != %v", initData, respData)
+		}
+	}
+
+	for _, test := range tests {
+		t.Run("Transport EarlyData Test", func(t *testing.T) {
+			noiseHandshake(t, test.initProtos, test.respProtos, test.expectedResult)
+		})
+	}
+}
diff --git a/p2p/transport/quic/tracer_test.go b/p2p/transport/internal/quicutils/tracer_test.go
similarity index 99%
rename from p2p/transport/quic/tracer_test.go
rename to p2p/transport/internal/quicutils/tracer_test.go
index c651032d..0d095d34 100644
--- a/p2p/transport/quic/tracer_test.go
+++ b/p2p/transport/internal/quicutils/tracer_test.go
@@ -1,4 +1,4 @@
-package libp2pquic
+package quicutils
 
 import (
 	"bytes"
diff --git a/p2p/transport/tcp/tcp_test.go b/p2p/transport/tcp/tcp_test.go
index f44c16c8..3d49a253 100644
--- a/p2p/transport/tcp/tcp_test.go
+++ b/p2p/transport/tcp/tcp_test.go
@@ -86,7 +86,7 @@ func TestResourceManager(t *testing.T) {
 		scope := mocknetwork.NewMockConnManagementScope(ctrl)
 		rcmgr.EXPECT().OpenConnection(network.DirOutbound, true, ln.Multiaddr()).Return(scope, nil)
 		scope.EXPECT().SetPeer(peerA)
-		scope.EXPECT().PeerScope().Return(network.NullScope).AnyTimes() // called by the upgrader
+		scope.EXPECT().PeerScope().Return(&network.NullScope{}).AnyTimes() // called by the upgrader
 		conn, err := tb.Dial(context.Background(), ln.Multiaddr(), peerA)
 		require.NoError(t, err)
 		scope.EXPECT().Done()
diff --git a/p2p/transport/websocket/websocket_test.go b/p2p/transport/websocket/websocket_test.go
index 1961e9ce..645dc824 100644
--- a/p2p/transport/websocket/websocket_test.go
+++ b/p2p/transport/websocket/websocket_test.go
@@ -9,10 +9,13 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
+	"errors"
 	"fmt"
 	"io"
 	"math/big"
 	"net"
+	"net/http"
+	"strings"
 	"testing"
 	"time"
 
@@ -79,7 +82,7 @@ func newSecureMuxer(t *testing.T) (peer.ID, sec.SecureMuxer) {
 		t.Fatal(err)
 	}
 	var secMuxer csms.SSMuxer
-	noiseTpt, err := noise.New(priv)
+	noiseTpt, err := noise.New(priv, nil)
 	require.NoError(t, err)
 	secMuxer.AddTransport(noise.ID, noiseTpt)
 	return id, &secMuxer
@@ -160,7 +163,7 @@ func testWSSServer(t *testing.T, listenAddr ma.Multiaddr) (ma.Multiaddr, peer.ID
 	}
 
 	id, u := newSecureUpgrader(t)
-	tpt, err := New(u, network.NullResourceManager, WithTLSConfig(tlsConf))
+	tpt, err := New(u, &network.NullResourceManager{}, WithTLSConfig(tlsConf))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -218,13 +221,51 @@ func getTLSConf(t *testing.T, ip net.IP, start, end time.Time) *tls.Config {
 	}
 }
 
+func TestHostHeaderWss(t *testing.T) {
+	server := &http.Server{}
+	l, err := net.Listen("tcp", ":0")
+	require.NoError(t, err)
+	defer server.Close()
+
+	errChan := make(chan error, 1)
+	go func() {
+		server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			defer close(errChan)
+			if !strings.Contains(r.Host, "example.com") {
+				errChan <- errors.New("Didn't see host header")
+			}
+			w.WriteHeader(http.StatusNotFound)
+		})
+		server.TLSConfig = getTLSConf(t, net.ParseIP("127.0.0.1"), time.Now(), time.Now().Add(time.Hour))
+		server.ServeTLS(l, "", "")
+	}()
+
+	_, port, err := net.SplitHostPort(l.Addr().String())
+	require.NoError(t, err)
+	serverMA := ma.StringCast("/ip4/127.0.0.1/tcp/" + port + "/tls/sni/example.com/ws")
+
+	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)
+
+	_, err = tpt.Dial(context.Background(), masToDial[0], test.RandPeerIDFatal(t))
+	require.Error(t, err)
+
+	err = <-errChan
+	require.NoError(t, err)
+}
+
 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))
+	tpt, err := New(u, &network.NullResourceManager{}, WithTLSClientConfig(tlsConfig))
 	require.NoError(t, err)
 
 	masToDial, err := tpt.Resolve(context.Background(), serverMA)
@@ -247,7 +288,7 @@ func TestDialWssNoClientCert(t *testing.T) {
 	require.Contains(t, serverMA.String(), "tls")
 
 	_, u := newSecureUpgrader(t)
-	tpt, err := New(u, network.NullResourceManager)
+	tpt, err := New(u, &network.NullResourceManager{})
 	require.NoError(t, err)
 
 	masToDial, err := tpt.Resolve(context.Background(), serverMA)
@@ -284,7 +325,7 @@ func connectAndExchangeData(t *testing.T, laddr ma.Multiaddr, secure bool) {
 		opts = append(opts, WithTLSConfig(tlsConf))
 	}
 	server, u := newUpgrader(t)
-	tpt, err := New(u, network.NullResourceManager, opts...)
+	tpt, err := New(u, &network.NullResourceManager{}, opts...)
 	require.NoError(t, err)
 	l, err := tpt.Listen(laddr)
 	require.NoError(t, err)
@@ -303,7 +344,7 @@ func connectAndExchangeData(t *testing.T, laddr ma.Multiaddr, secure bool) {
 			opts = append(opts, WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true}))
 		}
 		_, u := newUpgrader(t)
-		tpt, err := New(u, network.NullResourceManager, opts...)
+		tpt, err := New(u, &network.NullResourceManager{}, opts...)
 		require.NoError(t, err)
 		c, err := tpt.Dial(context.Background(), l.Multiaddr(), server)
 		require.NoError(t, err)
@@ -337,7 +378,7 @@ func TestWebsocketConnection(t *testing.T) {
 
 func TestWebsocketListenSecureFailWithoutTLSConfig(t *testing.T) {
 	_, u := newUpgrader(t)
-	tpt, err := New(u, network.NullResourceManager)
+	tpt, err := New(u, &network.NullResourceManager{})
 	require.NoError(t, err)
 	addr := ma.StringCast("/ip4/127.0.0.1/tcp/0/wss")
 	_, err = tpt.Listen(addr)
@@ -346,7 +387,7 @@ func TestWebsocketListenSecureFailWithoutTLSConfig(t *testing.T) {
 
 func TestWebsocketListenSecureAndInsecure(t *testing.T) {
 	serverID, serverUpgrader := newUpgrader(t)
-	server, err := New(serverUpgrader, network.NullResourceManager, WithTLSConfig(generateTLSConfig(t)))
+	server, err := New(serverUpgrader, &network.NullResourceManager{}, WithTLSConfig(generateTLSConfig(t)))
 	require.NoError(t, err)
 
 	lnInsecure, err := server.Listen(ma.StringCast("/ip4/127.0.0.1/tcp/0/ws"))
@@ -356,7 +397,7 @@ func TestWebsocketListenSecureAndInsecure(t *testing.T) {
 
 	t.Run("insecure", func(t *testing.T) {
 		_, clientUpgrader := newUpgrader(t)
-		client, err := New(clientUpgrader, network.NullResourceManager, WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true}))
+		client, err := New(clientUpgrader, &network.NullResourceManager{}, WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true}))
 		require.NoError(t, err)
 
 		// dialing the insecure address should succeed
@@ -373,7 +414,7 @@ func TestWebsocketListenSecureAndInsecure(t *testing.T) {
 
 	t.Run("secure", func(t *testing.T) {
 		_, clientUpgrader := newUpgrader(t)
-		client, err := New(clientUpgrader, network.NullResourceManager, WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true}))
+		client, err := New(clientUpgrader, &network.NullResourceManager{}, WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true}))
 		require.NoError(t, err)
 
 		// dialing the insecure address should succeed
@@ -391,7 +432,7 @@ func TestWebsocketListenSecureAndInsecure(t *testing.T) {
 
 func TestConcurrentClose(t *testing.T) {
 	_, u := newUpgrader(t)
-	tpt, err := New(u, network.NullResourceManager)
+	tpt, err := New(u, &network.NullResourceManager{})
 	require.NoError(t, err)
 	l, err := tpt.maListen(ma.StringCast("/ip4/127.0.0.1/tcp/0/ws"))
 	if err != nil {
@@ -429,7 +470,7 @@ func TestConcurrentClose(t *testing.T) {
 
 func TestWriteZero(t *testing.T) {
 	_, u := newUpgrader(t)
-	tpt, err := New(u, network.NullResourceManager)
+	tpt, err := New(u, &network.NullResourceManager{})
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/p2p/transport/webtransport/transport_test.go b/p2p/transport/webtransport/transport_test.go
index ea8d276a..de4cf226 100644
--- a/p2p/transport/webtransport/transport_test.go
+++ b/p2p/transport/webtransport/transport_test.go
@@ -1,6 +1,7 @@
 package libp2pwebtransport_test
 
 import (
+	"bytes"
 	"context"
 	"crypto/ecdsa"
 	"crypto/elliptic"
@@ -14,7 +15,10 @@ import (
 	"io"
 	"math/big"
 	"net"
+	"os"
+	"runtime"
 	"strings"
+	"sync/atomic"
 	"testing"
 	"time"
 
@@ -26,6 +30,7 @@ import (
 	libp2pwebtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
 
 	"github.com/golang/mock/gomock"
+	quicproxy "github.com/lucas-clemente/quic-go/integrationtests/tools/proxy"
 	ma "github.com/multiformats/go-multiaddr"
 	manet "github.com/multiformats/go-multiaddr/net"
 	"github.com/multiformats/go-multibase"
@@ -88,7 +93,7 @@ func getCerthashComponent(t *testing.T, b []byte) ma.Multiaddr {
 
 func TestTransport(t *testing.T) {
 	serverID, serverKey := newIdentity(t)
-	tr, err := libp2pwebtransport.New(serverKey, nil, network.NullResourceManager)
+	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"))
@@ -98,7 +103,7 @@ func TestTransport(t *testing.T) {
 	addrChan := make(chan ma.Multiaddr)
 	go func() {
 		_, clientKey := newIdentity(t)
-		tr2, err := libp2pwebtransport.New(clientKey, nil, network.NullResourceManager)
+		tr2, err := libp2pwebtransport.New(clientKey, nil, &network.NullResourceManager{})
 		require.NoError(t, err)
 		defer tr2.(io.Closer).Close()
 
@@ -134,7 +139,7 @@ func TestTransport(t *testing.T) {
 
 func TestHashVerification(t *testing.T) {
 	serverID, serverKey := newIdentity(t)
-	tr, err := libp2pwebtransport.New(serverKey, nil, network.NullResourceManager)
+	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"))
@@ -147,7 +152,7 @@ func TestHashVerification(t *testing.T) {
 	}()
 
 	_, clientKey := newIdentity(t)
-	tr2, err := libp2pwebtransport.New(clientKey, nil, network.NullResourceManager)
+	tr2, err := libp2pwebtransport.New(clientKey, nil, &network.NullResourceManager{})
 	require.NoError(t, err)
 	defer tr2.(io.Closer).Close()
 
@@ -185,7 +190,7 @@ func TestCanDial(t *testing.T) {
 	}
 
 	_, key := newIdentity(t)
-	tr, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+	tr, err := libp2pwebtransport.New(key, nil, &network.NullResourceManager{})
 	require.NoError(t, err)
 	defer tr.(io.Closer).Close()
 
@@ -211,7 +216,7 @@ func TestListenAddrValidity(t *testing.T) {
 	}
 
 	_, key := newIdentity(t)
-	tr, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+	tr, err := libp2pwebtransport.New(key, nil, &network.NullResourceManager{})
 	require.NoError(t, err)
 	defer tr.(io.Closer).Close()
 
@@ -228,7 +233,7 @@ func TestListenAddrValidity(t *testing.T) {
 
 func TestListenerAddrs(t *testing.T) {
 	_, key := newIdentity(t)
-	tr, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+	tr, err := libp2pwebtransport.New(key, nil, &network.NullResourceManager{})
 	require.NoError(t, err)
 	defer tr.(io.Closer).Close()
 
@@ -266,7 +271,7 @@ func TestResourceManagerDialing(t *testing.T) {
 
 func TestResourceManagerListening(t *testing.T) {
 	clientID, key := newIdentity(t)
-	cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+	cl, err := libp2pwebtransport.New(key, nil, &network.NullResourceManager{})
 	require.NoError(t, err)
 	defer cl.(io.Closer).Close()
 
@@ -345,7 +350,7 @@ func TestConnectionGaterDialing(t *testing.T) {
 	connGater := NewMockConnectionGater(ctrl)
 
 	serverID, serverKey := newIdentity(t)
-	tr, err := libp2pwebtransport.New(serverKey, nil, network.NullResourceManager)
+	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"))
@@ -356,7 +361,7 @@ func TestConnectionGaterDialing(t *testing.T) {
 		require.Equal(t, stripCertHashes(ln.Multiaddr()), addrs.RemoteMultiaddr())
 	})
 	_, key := newIdentity(t)
-	cl, err := libp2pwebtransport.New(key, connGater, network.NullResourceManager)
+	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)
@@ -369,7 +374,7 @@ func TestConnectionGaterInterceptAccept(t *testing.T) {
 	connGater := NewMockConnectionGater(ctrl)
 
 	serverID, serverKey := newIdentity(t)
-	tr, err := libp2pwebtransport.New(serverKey, connGater, network.NullResourceManager)
+	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"))
@@ -382,7 +387,7 @@ func TestConnectionGaterInterceptAccept(t *testing.T) {
 	})
 
 	_, key := newIdentity(t)
-	cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+	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)
@@ -395,7 +400,7 @@ func TestConnectionGaterInterceptSecured(t *testing.T) {
 	connGater := NewMockConnectionGater(ctrl)
 
 	serverID, serverKey := newIdentity(t)
-	tr, err := libp2pwebtransport.New(serverKey, connGater, network.NullResourceManager)
+	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"))
@@ -403,7 +408,7 @@ func TestConnectionGaterInterceptSecured(t *testing.T) {
 	defer ln.Close()
 
 	clientID, key := newIdentity(t)
-	cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+	cl, err := libp2pwebtransport.New(key, nil, &network.NullResourceManager{})
 	require.NoError(t, err)
 	defer cl.(io.Closer).Close()
 
@@ -461,7 +466,7 @@ 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))
+	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"))
@@ -471,7 +476,7 @@ func TestStaticTLSConf(t *testing.T) {
 
 	t.Run("fails when the certificate is invalid", func(t *testing.T) {
 		_, key := newIdentity(t)
-		cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+		cl, err := libp2pwebtransport.New(key, nil, &network.NullResourceManager{})
 		require.NoError(t, err)
 		defer cl.(io.Closer).Close()
 
@@ -485,7 +490,7 @@ func TestStaticTLSConf(t *testing.T) {
 
 	t.Run("fails when dialing with a wrong certhash", func(t *testing.T) {
 		_, key := newIdentity(t)
-		cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+		cl, err := libp2pwebtransport.New(key, nil, &network.NullResourceManager{})
 		require.NoError(t, err)
 		defer cl.(io.Closer).Close()
 
@@ -500,7 +505,7 @@ func TestStaticTLSConf(t *testing.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))
+		cl, err := libp2pwebtransport.New(key, nil, &network.NullResourceManager{}, libp2pwebtransport.WithTLSClientConfig(tlsConf))
 		require.NoError(t, err)
 		defer cl.(io.Closer).Close()
 
@@ -513,7 +518,7 @@ func TestStaticTLSConf(t *testing.T) {
 
 func TestAcceptQueueFilledUp(t *testing.T) {
 	serverID, serverKey := newIdentity(t)
-	tr, err := libp2pwebtransport.New(serverKey, nil, network.NullResourceManager)
+	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"))
@@ -523,7 +528,7 @@ func TestAcceptQueueFilledUp(t *testing.T) {
 	newConn := func() (tpt.CapableConn, error) {
 		t.Helper()
 		_, key := newIdentity(t)
-		cl, err := libp2pwebtransport.New(key, nil, network.NullResourceManager)
+		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)
@@ -553,7 +558,7 @@ func TestSNIIsSent(t *testing.T) {
 			return tlsConf, nil
 		},
 	}
-	tr, err := libp2pwebtransport.New(key, nil, network.NullResourceManager, libp2pwebtransport.WithTLSConfig(tlsConf))
+	tr, err := libp2pwebtransport.New(key, nil, &network.NullResourceManager{}, libp2pwebtransport.WithTLSConfig(tlsConf))
 	require.NoError(t, err)
 	defer tr.(io.Closer).Close()
 
@@ -561,7 +566,7 @@ func TestSNIIsSent(t *testing.T) {
 	require.NoError(t, err)
 
 	_, key2 := newIdentity(t)
-	clientTr, err := libp2pwebtransport.New(key2, nil, network.NullResourceManager)
+	clientTr, err := libp2pwebtransport.New(key2, nil, &network.NullResourceManager{})
 	require.NoError(t, err)
 	defer tr.(io.Closer).Close()
 
@@ -582,5 +587,130 @@ func TestSNIIsSent(t *testing.T) {
 	case <-time.After(time.Minute):
 		t.Fatalf("Expected to get server name")
 	}
+}
+
+type reportingRcmgr struct {
+	network.NullResourceManager
+	report chan<- int
+}
+
+func (m *reportingRcmgr) OpenConnection(dir network.Direction, usefd bool, endpoint ma.Multiaddr) (network.ConnManagementScope, error) {
+	return &reportingScope{report: m.report}, nil
+}
+
+type reportingScope struct {
+	network.NullScope
+	report chan<- int
+}
+
+func (s *reportingScope) ReserveMemory(size int, _ uint8) error {
+	s.report <- size
+	return nil
+}
+
+func TestFlowControlWindowIncrease(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("this test is flaky on Windows")
+	}
+
+	rtt := 10 * time.Millisecond
+	timeout := 5 * time.Second
+
+	if os.Getenv("CI") != "" {
+		rtt = 40 * time.Millisecond
+		timeout = 15 * time.Second
+	}
+
+	serverID, serverKey := newIdentity(t)
+	serverWindowIncreases := make(chan int, 100)
+	serverRcmgr := &reportingRcmgr{report: serverWindowIncreases}
+	tr, err := libp2pwebtransport.New(serverKey, nil, serverRcmgr)
+	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()
+
+	go func() {
+		conn, err := ln.Accept()
+		require.NoError(t, err)
+		str, err := conn.AcceptStream()
+		require.NoError(t, err)
+		_, err = io.CopyBuffer(str, str, make([]byte, 2<<10))
+		require.NoError(t, err)
+		str.CloseWrite()
+	}()
+
+	proxy, err := quicproxy.NewQuicProxy("localhost:0", &quicproxy.Opts{
+		RemoteAddr:  ln.Addr().String(),
+		DelayPacket: func(quicproxy.Direction, []byte) time.Duration { return rtt / 2 },
+	})
+	require.NoError(t, err)
+	defer proxy.Close()
 
+	_, clientKey := newIdentity(t)
+	clientWindowIncreases := make(chan int, 100)
+	clientRcmgr := &reportingRcmgr{report: clientWindowIncreases}
+	tr2, err := libp2pwebtransport.New(clientKey, nil, clientRcmgr)
+	require.NoError(t, err)
+	defer tr2.(io.Closer).Close()
+
+	var addr ma.Multiaddr
+	for _, comp := range ma.Split(ln.Multiaddr()) {
+		if _, err := comp.ValueForProtocol(ma.P_UDP); err == nil {
+			addr = addr.Encapsulate(ma.StringCast(fmt.Sprintf("/udp/%d", proxy.LocalPort())))
+			continue
+		}
+		if addr == nil {
+			addr = comp
+			continue
+		}
+		addr = addr.Encapsulate(comp)
+	}
+
+	conn, err := tr2.Dial(context.Background(), addr, serverID)
+	require.NoError(t, err)
+	str, err := conn.OpenStream(context.Background())
+	require.NoError(t, err)
+	var increasesDone uint32 // to be used atomically
+	go func() {
+		for {
+			_, err := str.Write(bytes.Repeat([]byte{0x42}, 1<<10))
+			require.NoError(t, err)
+			if atomic.LoadUint32(&increasesDone) > 0 {
+				str.CloseWrite()
+				return
+			}
+		}
+	}()
+	done := make(chan struct{})
+	go func() {
+		defer close(done)
+		_, err := io.ReadAll(str)
+		require.NoError(t, err)
+	}()
+
+	var numServerIncreases, numClientIncreases int
+	timer := time.NewTimer(timeout)
+	defer timer.Stop()
+	for {
+		select {
+		case <-serverWindowIncreases:
+			numServerIncreases++
+		case <-clientWindowIncreases:
+			numClientIncreases++
+		case <-timer.C:
+			t.Fatalf("didn't receive enough window increases (client: %d, server: %d)", numClientIncreases, numServerIncreases)
+		}
+		if numClientIncreases >= 1 && numServerIncreases >= 1 {
+			atomic.AddUint32(&increasesDone, 1)
+			break
+		}
+	}
+
+	select {
+	case <-done:
+	case <-time.After(timeout):
+		t.Fatal("timeout")
+	}
 }