Repository: quic-go/quic-go
Pull request #3809 (2023-05-06 11:18:32)

ci: fix ordering of error output of the cross compilation workflow

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/quic-go/quic-go/pull/3809?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#3809](https://app.codecov.io/gh/quic-go/quic-go/pull/3809?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (9b9b6c9) into [master](https://app.codecov.io/gh/quic-go/quic-go/commit/22d6b0e626c56f311ff05705042d8caf506e5919?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (22d6b0e) will **increase** coverage by `0.02%`.
> The diff coverage is `n/a`.

```diff
@@            Coverage Diff             @@
##           master    #3809      +/-   ##
==========================================
+ Coverage   83.92%   83.95%   +0.02%     
==========================================
  Files         142      142              
  Lines       14208    14208              
==========================================
+ Hits        11924    11927       +3     
+ Misses       1850     1848       -2     
+ Partials      434      433       -1     
```


[see 2 files with indirect coverage changes](https://app.codecov.io/gh/quic-go/quic-go/pull/3809/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)



Patch:
diff --git a/.github/workflows/cross-compile.sh b/.github/workflows/cross-compile.sh
index ff5b67fc..cd526223 100755
--- a/.github/workflows/cross-compile.sh
+++ b/.github/workflows/cross-compile.sh
@@ -12,7 +12,22 @@ if [[ "$goos" == "android" ]]; then exit; fi
 # Cgo would then need a C cross compilation setup. Not worth the hassle.
 if [[ "$goos" == "ios" ]]; then exit; fi
 
-echo "$dist"
+# Write all log output to a temporary file instead of to stdout.
+# That allows running this script in parallel, while preserving the correct order of the output.
+log_file=$(mktemp)
+
+error_handler() {
+  cat "$log_file" >&2
+  rm "$log_file"
+  exit 1
+}
+
+trap 'error_handler' ERR
+
+echo "$dist" >> "$log_file"
 out="main-$goos-$goarch"
-GOOS=$goos GOARCH=$goarch go build -o $out example/main.go
+GOOS=$goos GOARCH=$goarch go build -o $out example/main.go >> "$log_file" 2>&1
 rm $out
+
+cat "$log_file"
+rm "$log_file"
diff --git a/.github/workflows/go-generate.sh b/.github/workflows/go-generate.sh
index cd84098e..2d1b9447 100755
--- a/.github/workflows/go-generate.sh
+++ b/.github/workflows/go-generate.sh
@@ -11,6 +11,10 @@ cp -r "$DIR" generated
 cd generated
 # delete all go-generated files generated (that adhere to the comment convention)
 grep --include \*.go -lrIZ "^// Code generated .* DO NOT EDIT\.$" . | xargs --null rm
+
+# First regenerate sys_conn_buffers_write.go.
+# If it doesn't exist, the following mockgen calls will fail.
+go generate -run "sys_conn_buffers_write.go"
 # now generate everything
 go generate ./...
 cd ..
diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml
index 6acfdc15..eb4ce725 100644
--- a/.github/workflows/unit.yml
+++ b/.github/workflows/unit.yml
@@ -20,6 +20,16 @@ jobs:
         env:
           TIMESCALE_FACTOR: 10
         run: go run github.com/onsi/ginkgo/v2/ginkgo -r -v -cover -randomize-all -randomize-suites -trace -skip-package integrationtests
+      - name: Run tests as root
+        if: ${{ matrix.os == 'ubuntu' }}
+        env:
+          TIMESCALE_FACTOR: 10
+          FILE: sys_conn_helper_linux_test.go
+        run: |
+          test -f $FILE # make sure the file actually exists
+          go run github.com/onsi/ginkgo/v2/ginkgo build -cover -tags root .
+          sudo ./quic-go.test -ginkgo.v -ginkgo.trace -ginkgo.randomize-all -ginkgo.focus-file=$FILE -test.coverprofile coverage-root.txt
+          rm quic-go.test
       - name: Run tests (32 bit)
         if: ${{ matrix.os != 'macos' }} # can't run 32 bit tests on OSX.
         env:
@@ -34,5 +44,5 @@ jobs:
       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v3
         with:
-          file: coverage.txt
+          files: coverage.txt,coverage-root.txt
           env_vars: OS=${{ matrix.os }}, GO=${{ matrix.go }}
diff --git a/README.md b/README.md
index e518f1e4..81a37b4a 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,12 @@
 [![PkgGoDev](https://pkg.go.dev/badge/github.com/quic-go/quic-go)](https://pkg.go.dev/github.com/quic-go/quic-go)
 [![Code Coverage](https://img.shields.io/codecov/c/github/quic-go/quic-go/master.svg?style=flat-square)](https://codecov.io/gh/quic-go/quic-go/)
 
-quic-go is an implementation of the QUIC protocol ([RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000), [RFC 9001](https://datatracker.ietf.org/doc/html/rfc9001), [RFC 9002](https://datatracker.ietf.org/doc/html/rfc9002)) in Go, including the Unreliable Datagram Extension ([RFC 9221](https://datatracker.ietf.org/doc/html/rfc9221)) and Datagram Packetization Layer Path MTU
-   Discovery (DPLPMTUD, [RFC 8899](https://datatracker.ietf.org/doc/html/rfc8899)). It has support for HTTP/3 ([RFC 9114](https://datatracker.ietf.org/doc/html/rfc9114)), including QPACK ([RFC 9204](https://datatracker.ietf.org/doc/html/rfc9204)).
+quic-go is an implementation of the QUIC protocol ([RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000), [RFC 9001](https://datatracker.ietf.org/doc/html/rfc9001), [RFC 9002](https://datatracker.ietf.org/doc/html/rfc9002)) in Go. It has support for HTTP/3 ([RFC 9114](https://datatracker.ietf.org/doc/html/rfc9114)), including QPACK ([RFC 9204](https://datatracker.ietf.org/doc/html/rfc9204)).
+
+In addition to these base PR, it also implements the following RFS: 
+* Unreliable Datagram Extension ([RFC 9221](https://datatracker.ietf.org/doc/html/rfc9221))
+* Datagram Packetization Layer Path MTU Discovery (DPLPMTUD, [RFC 8899](https://datatracker.ietf.org/doc/html/rfc8899))
+* QUIC Version 2 ([RFC 9369](https://datatracker.ietf.org/doc/html/rfc9369))
 
 In addition to the RFCs listed above, it currently implements the [IETF QUIC draft-29](https://tools.ietf.org/html/draft-ietf-quic-transport-29). Support for draft-29 will eventually be dropped, as it is phased out of the ecosystem.
 
diff --git a/connection.go b/connection.go
index 6833a7b9..c4a91a35 100644
--- a/connection.go
+++ b/connection.go
@@ -312,9 +312,14 @@ var newConnection = func(
 		DisableActiveMigration:          true,
 		StatelessResetToken:             &statelessResetToken,
 		OriginalDestinationConnectionID: origDestConnID,
-		ActiveConnectionIDLimit:         protocol.MaxActiveConnectionIDs,
-		InitialSourceConnectionID:       srcConnID,
-		RetrySourceConnectionID:         retrySrcConnID,
+		// For interoperability with quic-go versions before May 2023, this value must be set to a value
+		// different from protocol.DefaultActiveConnectionIDLimit.
+		// If set to the default value, it will be omitted from the transport parameters, which will make
+		// old quic-go versions interpret it as 0, instead of the default value of 2.
+		// See https://github.com/quic-go/quic-go/pull/3806.
+		ActiveConnectionIDLimit:   protocol.MaxActiveConnectionIDs,
+		InitialSourceConnectionID: srcConnID,
+		RetrySourceConnectionID:   retrySrcConnID,
 	}
 	if s.config.EnableDatagrams {
 		params.MaxDatagramFrameSize = protocol.MaxDatagramFrameSize
@@ -426,8 +431,13 @@ var newClientConnection = func(
 		MaxAckDelay:                    protocol.MaxAckDelayInclGranularity,
 		AckDelayExponent:               protocol.AckDelayExponent,
 		DisableActiveMigration:         true,
-		ActiveConnectionIDLimit:        protocol.MaxActiveConnectionIDs,
-		InitialSourceConnectionID:      srcConnID,
+		// For interoperability with quic-go versions before May 2023, this value must be set to a value
+		// different from protocol.DefaultActiveConnectionIDLimit.
+		// If set to the default value, it will be omitted from the transport parameters, which will make
+		// old quic-go versions interpret it as 0, instead of the default value of 2.
+		// See https://github.com/quic-go/quic-go/pull/3806.
+		ActiveConnectionIDLimit:   protocol.MaxActiveConnectionIDs,
+		InitialSourceConnectionID: srcConnID,
 	}
 	if s.config.EnableDatagrams {
 		params.MaxDatagramFrameSize = protocol.MaxDatagramFrameSize
diff --git a/framer.go b/framer.go
index 0b205916..79c28e23 100644
--- a/framer.go
+++ b/framer.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/quic-go/quic-go/internal/ackhandler"
 	"github.com/quic-go/quic-go/internal/protocol"
+	"github.com/quic-go/quic-go/internal/utils/ringbuffer"
 	"github.com/quic-go/quic-go/internal/wire"
 	"github.com/quic-go/quic-go/quicvarint"
 )
@@ -28,7 +29,7 @@ type framerI struct {
 	streamGetter streamGetter
 
 	activeStreams map[protocol.StreamID]struct{}
-	streamQueue   []protocol.StreamID
+	streamQueue   ringbuffer.RingBuffer[protocol.StreamID]
 
 	controlFrameMutex sync.Mutex
 	controlFrames     []wire.Frame
@@ -45,7 +46,7 @@ func newFramer(streamGetter streamGetter) framer {
 
 func (f *framerI) HasData() bool {
 	f.mutex.Lock()
-	hasData := len(f.streamQueue) > 0
+	hasData := !f.streamQueue.Empty()
 	f.mutex.Unlock()
 	if hasData {
 		return true
@@ -84,7 +85,7 @@ func (f *framerI) AppendControlFrames(frames []*ackhandler.Frame, maxLen protoco
 func (f *framerI) AddActiveStream(id protocol.StreamID) {
 	f.mutex.Lock()
 	if _, ok := f.activeStreams[id]; !ok {
-		f.streamQueue = append(f.streamQueue, id)
+		f.streamQueue.PushBack(id)
 		f.activeStreams[id] = struct{}{}
 	}
 	f.mutex.Unlock()
@@ -95,13 +96,12 @@ func (f *framerI) AppendStreamFrames(frames []*ackhandler.Frame, maxLen protocol
 	var lastFrame *ackhandler.Frame
 	f.mutex.Lock()
 	// pop STREAM frames, until less than MinStreamFrameSize bytes are left in the packet
-	numActiveStreams := len(f.streamQueue)
+	numActiveStreams := f.streamQueue.Len()
 	for i := 0; i < numActiveStreams; i++ {
 		if protocol.MinStreamFrameSize+length > maxLen {
 			break
 		}
-		id := f.streamQueue[0]
-		f.streamQueue = f.streamQueue[1:]
+		id := f.streamQueue.PopFront()
 		// This should never return an error. Better check it anyway.
 		// The stream will only be in the streamQueue, if it enqueued itself there.
 		str, err := f.streamGetter.GetOrOpenSendStream(id)
@@ -117,7 +117,7 @@ func (f *framerI) AppendStreamFrames(frames []*ackhandler.Frame, maxLen protocol
 		remainingLen += quicvarint.Len(uint64(remainingLen))
 		frame, hasMoreData := str.popStreamFrame(remainingLen, v)
 		if hasMoreData { // put the stream back in the queue (at the end)
-			f.streamQueue = append(f.streamQueue, id)
+			f.streamQueue.PushBack(id)
 		} else { // no more data to send. Stream is not active any more
 			delete(f.activeStreams, id)
 		}
@@ -146,7 +146,7 @@ func (f *framerI) Handle0RTTRejection() error {
 	defer f.mutex.Unlock()
 
 	f.controlFrameMutex.Lock()
-	f.streamQueue = f.streamQueue[:0]
+	f.streamQueue.Clear()
 	for id := range f.activeStreams {
 		delete(f.activeStreams, id)
 	}
diff --git a/go.mod b/go.mod
index bdb8183f..9cdfd0d6 100644
--- a/go.mod
+++ b/go.mod
@@ -5,25 +5,26 @@ go 1.19
 require (
 	github.com/francoispqt/gojay v1.2.13
 	github.com/golang/mock v1.6.0
-	github.com/onsi/ginkgo/v2 v2.2.0
-	github.com/onsi/gomega v1.20.1
+	github.com/onsi/ginkgo/v2 v2.9.5
+	github.com/onsi/gomega v1.27.6
 	github.com/quic-go/qpack v0.4.0
 	github.com/quic-go/qtls-go1-19 v0.3.2
 	github.com/quic-go/qtls-go1-20 v0.2.2
 	golang.org/x/crypto v0.4.0
 	golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
-	golang.org/x/net v0.7.0
-	golang.org/x/sync v0.1.0
-	golang.org/x/sys v0.5.0
+	golang.org/x/net v0.10.0
+	golang.org/x/sync v0.2.0
+	golang.org/x/sys v0.8.0
 )
 
 require (
-	github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
-	github.com/google/go-cmp v0.5.8 // indirect
+	github.com/go-logr/logr v1.2.4 // indirect
+	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
-	golang.org/x/mod v0.6.0 // indirect
-	golang.org/x/text v0.7.0 // indirect
-	golang.org/x/tools v0.2.0 // indirect
+	golang.org/x/mod v0.10.0 // indirect
+	golang.org/x/text v0.9.0 // indirect
+	golang.org/x/tools v0.9.1 // indirect
 	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index a2375b03..3a777327 100644
--- a/go.sum
+++ b/go.sum
@@ -28,8 +28,10 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
-github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
-github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
@@ -39,11 +41,11 @@ github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
 github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@@ -74,10 +76,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
 github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
-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.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
-github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
+github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
+github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
+github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
+github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
 github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -120,8 +122,8 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh
 github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
 github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
 github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
@@ -142,8 +144,8 @@ golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTk
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
-golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
+golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
+golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -155,8 +157,8 @@ golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -168,8 +170,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
+golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -180,14 +182,14 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -197,8 +199,8 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
-golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
+golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
+golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -225,7 +227,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
diff --git a/http3/client.go b/http3/client.go
index 9c94a31e..c54de5ea 100644
--- a/http3/client.go
+++ b/http3/client.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"net"
 	"net/http"
 	"strconv"
 	"sync"
@@ -92,6 +93,14 @@ func newClient(hostname string, tlsConf *tls.Config, opts *roundTripperOpts, con
 	} else {
 		tlsConf = tlsConf.Clone()
 	}
+	if tlsConf.ServerName == "" {
+		sni, _, err := net.SplitHostPort(hostname)
+		if err != nil {
+			// It's ok if net.SplitHostPort returns an error - it could be a hostname/IP address without a port.
+			sni = hostname
+		}
+		tlsConf.ServerName = sni
+	}
 	// Replace existing ALPNs by H3
 	tlsConf.NextProtos = []string{versionToALPN(conf.Versions[0])}
 
diff --git a/http3/roundtrip.go b/http3/roundtrip.go
index 53885e1e..066b762b 100644
--- a/http3/roundtrip.go
+++ b/http3/roundtrip.go
@@ -10,21 +10,24 @@ import (
 	"net/http"
 	"strings"
 	"sync"
+	"sync/atomic"
 
 	"golang.org/x/net/http/httpguts"
 
 	"github.com/quic-go/quic-go"
 )
 
-// declare this as a variable, such that we can it mock it in the tests
-var quicDialer = quic.DialEarly
-
 type roundTripCloser interface {
 	RoundTripOpt(*http.Request, RoundTripOpt) (*http.Response, error)
 	HandshakeComplete() bool
 	io.Closer
 }
 
+type roundTripCloserWithCount struct {
+	roundTripCloser
+	useCount atomic.Int64
+}
+
 // RoundTripper implements the http.RoundTripper interface
 type RoundTripper struct {
 	mutex sync.Mutex
@@ -82,8 +85,8 @@ type RoundTripper struct {
 	MaxResponseHeaderBytes int64
 
 	newClient func(hostname string, tlsConf *tls.Config, opts *roundTripperOpts, conf *quic.Config, dialer dialFunc) (roundTripCloser, error) // so we can mock it in tests
-	clients   map[string]roundTripCloser
-	udpConn   *net.UDPConn
+	clients   map[string]*roundTripCloserWithCount
+	transport *quic.Transport
 }
 
 // RoundTripOpt are options for the Transport.RoundTripOpt method.
@@ -143,6 +146,7 @@ func (r *RoundTripper) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.
 	if err != nil {
 		return nil, err
 	}
+	defer cl.useCount.Add(-1)
 	rsp, err := cl.RoundTripOpt(req, opt)
 	if err != nil {
 		r.removeClient(hostname)
@@ -160,12 +164,12 @@ func (r *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
 	return r.RoundTripOpt(req, RoundTripOpt{})
 }
 
-func (r *RoundTripper) getClient(hostname string, onlyCached bool) (rtc roundTripCloser, isReused bool, err error) {
+func (r *RoundTripper) getClient(hostname string, onlyCached bool) (rtc *roundTripCloserWithCount, isReused bool, err error) {
 	r.mutex.Lock()
 	defer r.mutex.Unlock()
 
 	if r.clients == nil {
-		r.clients = make(map[string]roundTripCloser)
+		r.clients = make(map[string]*roundTripCloserWithCount)
 	}
 
 	client, ok := r.clients[hostname]
@@ -180,15 +184,16 @@ func (r *RoundTripper) getClient(hostname string, onlyCached bool) (rtc roundTri
 		}
 		dial := r.Dial
 		if dial == nil {
-			if r.udpConn == nil {
-				r.udpConn, err = net.ListenUDP("udp", nil)
+			if r.transport == nil {
+				udpConn, err := net.ListenUDP("udp", nil)
 				if err != nil {
 					return nil, false, err
 				}
+				r.transport = &quic.Transport{Conn: udpConn}
 			}
 			dial = r.makeDialer()
 		}
-		client, err = newCl(
+		c, err := newCl(
 			hostname,
 			r.TLSClientConfig,
 			&roundTripperOpts{
@@ -204,10 +209,12 @@ func (r *RoundTripper) getClient(hostname string, onlyCached bool) (rtc roundTri
 		if err != nil {
 			return nil, false, err
 		}
+		client = &roundTripCloserWithCount{roundTripCloser: c}
 		r.clients[hostname] = client
 	} else if client.HandshakeComplete() {
 		isReused = true
 	}
+	client.useCount.Add(1)
 	return client, isReused, nil
 }
 
@@ -231,9 +238,14 @@ func (r *RoundTripper) Close() error {
 		}
 	}
 	r.clients = nil
-	if r.udpConn != nil {
-		r.udpConn.Close()
-		r.udpConn = nil
+	if r.transport != nil {
+		if err := r.transport.Close(); err != nil {
+			return err
+		}
+		if err := r.transport.Conn.Close(); err != nil {
+			return err
+		}
+		r.transport = nil
 	}
 	return nil
 }
@@ -273,6 +285,17 @@ func (r *RoundTripper) makeDialer() func(ctx context.Context, addr string, tlsCf
 		if err != nil {
 			return nil, err
 		}
-		return quicDialer(ctx, r.udpConn, udpAddr, tlsCfg, cfg)
+		return r.transport.DialEarly(ctx, udpAddr, tlsCfg, cfg)
+	}
+}
+
+func (r *RoundTripper) CloseIdleConnections() {
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+	for hostname, client := range r.clients {
+		if client.useCount.Load() == 0 {
+			client.Close()
+			delete(r.clients, hostname)
+		}
 	}
 }
diff --git a/internal/ackhandler/packet.go b/internal/ackhandler/packet.go
index 394ee40a..849eea69 100644
--- a/internal/ackhandler/packet.go
+++ b/internal/ackhandler/packet.go
@@ -9,12 +9,12 @@ import (
 
 // A Packet is a packet
 type Packet struct {
+	SendTime        time.Time
 	PacketNumber    protocol.PacketNumber
 	Frames          []*Frame
 	LargestAcked    protocol.PacketNumber // InvalidPacketNumber if the packet doesn't contain an ACK
 	Length          protocol.ByteCount
 	EncryptionLevel protocol.EncryptionLevel
-	SendTime        time.Time
 
 	IsPathMTUProbePacket bool // We don't report the loss of Path MTU probe packets to the congestion controller.
 
diff --git a/internal/ackhandler/sent_packet_handler.go b/internal/ackhandler/sent_packet_handler.go
index 732bbc3a..550dc74d 100644
--- a/internal/ackhandler/sent_packet_handler.go
+++ b/internal/ackhandler/sent_packet_handler.go
@@ -234,11 +234,31 @@ func (h *sentPacketHandler) SentPacket(p *Packet) {
 	if h.perspective == protocol.PerspectiveClient && p.EncryptionLevel == protocol.EncryptionHandshake && h.initialPackets != nil {
 		h.dropPackets(protocol.EncryptionInitial)
 	}
-	isAckEliciting := h.sentPacketImpl(p)
+
+	pnSpace := h.getPacketNumberSpace(p.EncryptionLevel)
+	if h.logger.Debug() && pnSpace.history.HasOutstandingPackets() {
+		for pn := utils.Max(0, pnSpace.largestSent+1); pn < p.PacketNumber; pn++ {
+			h.logger.Debugf("Skipping packet number %d", pn)
+		}
+	}
+
+	pnSpace.largestSent = p.PacketNumber
+	isAckEliciting := len(p.Frames) > 0
+
 	if isAckEliciting {
-		h.getPacketNumberSpace(p.EncryptionLevel).history.SentAckElicitingPacket(p)
+		pnSpace.lastAckElicitingPacketTime = p.SendTime
+		p.includedInBytesInFlight = true
+		h.bytesInFlight += p.Length
+		if h.numProbesToSend > 0 {
+			h.numProbesToSend--
+		}
+	}
+	h.congestion.OnPacketSent(p.SendTime, h.bytesInFlight, p.PacketNumber, p.Length, isAckEliciting)
+
+	if isAckEliciting {
+		pnSpace.history.SentAckElicitingPacket(p)
 	} else {
-		h.getPacketNumberSpace(p.EncryptionLevel).history.SentNonAckElicitingPacket(p.PacketNumber, p.EncryptionLevel, p.SendTime)
+		pnSpace.history.SentNonAckElicitingPacket(p.PacketNumber, p.EncryptionLevel, p.SendTime)
 		putPacket(p)
 		p = nil //nolint:ineffassign // This is just to be on the safe side.
 	}
@@ -263,31 +283,6 @@ func (h *sentPacketHandler) getPacketNumberSpace(encLevel protocol.EncryptionLev
 	}
 }
 
-func (h *sentPacketHandler) sentPacketImpl(packet *Packet) bool /* is ack-eliciting */ {
-	pnSpace := h.getPacketNumberSpace(packet.EncryptionLevel)
-
-	if h.logger.Debug() && pnSpace.history.HasOutstandingPackets() {
-		for p := utils.Max(0, pnSpace.largestSent+1); p < packet.PacketNumber; p++ {
-			h.logger.Debugf("Skipping packet number %d", p)
-		}
-	}
-
-	pnSpace.largestSent = packet.PacketNumber
-	isAckEliciting := len(packet.Frames) > 0
-
-	if isAckEliciting {
-		pnSpace.lastAckElicitingPacketTime = packet.SendTime
-		packet.includedInBytesInFlight = true
-		h.bytesInFlight += packet.Length
-		if h.numProbesToSend > 0 {
-			h.numProbesToSend--
-		}
-	}
-	h.congestion.OnPacketSent(packet.SendTime, h.bytesInFlight, packet.PacketNumber, packet.Length, isAckEliciting)
-
-	return isAckEliciting
-}
-
 func (h *sentPacketHandler) ReceivedAck(ack *wire.AckFrame, encLevel protocol.EncryptionLevel, rcvTime time.Time) (bool /* contained 1-RTT packet */, error) {
 	pnSpace := h.getPacketNumberSpace(encLevel)
 
diff --git a/internal/protocol/params.go b/internal/protocol/params.go
index 60c86779..fe3a7562 100644
--- a/internal/protocol/params.go
+++ b/internal/protocol/params.go
@@ -5,6 +5,9 @@ import "time"
 // DesiredReceiveBufferSize is the kernel UDP receive buffer size that we'd like to use.
 const DesiredReceiveBufferSize = (1 << 20) * 2 // 2 MB
 
+// DesiredSendBufferSize is the kernel UDP send buffer size that we'd like to use.
+const DesiredSendBufferSize = (1 << 20) * 2 // 2 MB
+
 // InitialPacketSizeIPv4 is the maximum packet size that we use for sending IPv4 packets.
 const InitialPacketSizeIPv4 = 1252
 
diff --git a/internal/protocol/protocol.go b/internal/protocol/protocol.go
index 8241e274..93ad96cc 100644
--- a/internal/protocol/protocol.go
+++ b/internal/protocol/protocol.go
@@ -77,6 +77,9 @@ const MinConnectionIDLenInitial = 8
 // DefaultAckDelayExponent is the default ack delay exponent
 const DefaultAckDelayExponent = 3
 
+// DefaultActiveConnectionIDLimit is the default active connection ID limit
+const DefaultActiveConnectionIDLimit = 2
+
 // MaxAckDelayExponent is the maximum ack delay exponent
 const MaxAckDelayExponent = 20
 
diff --git a/internal/utils/ringbuffer/ringbuffer.go b/internal/utils/ringbuffer/ringbuffer.go
new file mode 100644
index 00000000..81a5ad44
--- /dev/null
+++ b/internal/utils/ringbuffer/ringbuffer.go
@@ -0,0 +1,86 @@
+package ringbuffer
+
+// A RingBuffer is a ring buffer.
+// It acts as a heap that doesn't cause any allocations.
+type RingBuffer[T any] struct {
+	ring             []T
+	headPos, tailPos int
+	full             bool
+}
+
+// Init preallocs a buffer with a certain size.
+func (r *RingBuffer[T]) Init(size int) {
+	r.ring = make([]T, size)
+}
+
+// Len returns the number of elements in the ring buffer.
+func (r *RingBuffer[T]) Len() int {
+	if r.full {
+		return len(r.ring)
+	}
+	if r.tailPos >= r.headPos {
+		return r.tailPos - r.headPos
+	}
+	return r.tailPos - r.headPos + len(r.ring)
+}
+
+// Empty says if the ring buffer is empty.
+func (r *RingBuffer[T]) Empty() bool {
+	return !r.full && r.headPos == r.tailPos
+}
+
+// PushBack adds a new element.
+// If the ring buffer is full, its capacity is increased first.
+func (r *RingBuffer[T]) PushBack(t T) {
+	if r.full || len(r.ring) == 0 {
+		r.grow()
+	}
+	r.ring[r.tailPos] = t
+	r.tailPos++
+	if r.tailPos == len(r.ring) {
+		r.tailPos = 0
+	}
+	if r.tailPos == r.headPos {
+		r.full = true
+	}
+}
+
+// PopFront returns the next element.
+// It must not be called when the buffer is empty, that means that
+// callers might need to check if there are elements in the buffer first.
+func (r *RingBuffer[T]) PopFront() T {
+	if r.Empty() {
+		panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: pop from an empty queue")
+	}
+	r.full = false
+	t := r.ring[r.headPos]
+	r.ring[r.headPos] = *new(T)
+	r.headPos++
+	if r.headPos == len(r.ring) {
+		r.headPos = 0
+	}
+	return t
+}
+
+// Grow the maximum size of the queue.
+// This method assume the queue is full.
+func (r *RingBuffer[T]) grow() {
+	oldRing := r.ring
+	newSize := len(oldRing) * 2
+	if newSize == 0 {
+		newSize = 1
+	}
+	r.ring = make([]T, newSize)
+	headLen := copy(r.ring, oldRing[r.headPos:])
+	copy(r.ring[headLen:], oldRing[:r.headPos])
+	r.headPos, r.tailPos, r.full = 0, len(oldRing), false
+}
+
+// Clear removes all elements.
+func (r *RingBuffer[T]) Clear() {
+	var zeroValue T
+	for i := range r.ring {
+		r.ring[i] = zeroValue
+	}
+	r.headPos, r.tailPos, r.full = 0, 0, false
+}
diff --git a/internal/utils/rtt_stats.go b/internal/utils/rtt_stats.go
index 527539e1..2cd9a191 100644
--- a/internal/utils/rtt_stats.go
+++ b/internal/utils/rtt_stats.go
@@ -103,8 +103,12 @@ func (r *RTTStats) SetMaxAckDelay(mad time.Duration) {
 // SetInitialRTT sets the initial RTT.
 // It is used during the 0-RTT handshake when restoring the RTT stats from the session state.
 func (r *RTTStats) SetInitialRTT(t time.Duration) {
+	// On the server side, by the time we get to process the session ticket,
+	// we might already have obtained an RTT measurement.
+	// This can happen if we received the ClientHello in multiple pieces, and one of those pieces was lost.
+	// Discard the restored value. A fresh measurement is always better.
 	if r.hasMeasurement {
-		panic("initial RTT set after first measurement")
+		return
 	}
 	r.smoothedRTT = t
 	r.latestRTT = t
diff --git a/internal/wire/ack_frame.go b/internal/wire/ack_frame.go
index f145c8b4..2f816c78 100644
--- a/internal/wire/ack_frame.go
+++ b/internal/wire/ack_frame.go
@@ -90,13 +90,22 @@ func parseAckFrame(r *bytes.Reader, typ uint64, ackDelayExponent uint8, _ protoc
 		return nil, errInvalidAckRanges
 	}
 
-	// parse (and skip) the ECN section
 	if ecn {
-		for i := 0; i < 3; i++ {
-			if _, err := quicvarint.Read(r); err != nil {
-				return nil, err
-			}
+		ect0, err := quicvarint.Read(r)
+		if err != nil {
+			return nil, err
+		}
+		frame.ECT0 = ect0
+		ect1, err := quicvarint.Read(r)
+		if err != nil {
+			return nil, err
+		}
+		frame.ECT1 = ect1
+		ecnce, err := quicvarint.Read(r)
+		if err != nil {
+			return nil, err
 		}
+		frame.ECNCE = ecnce
 	}
 
 	return frame, nil
diff --git a/internal/wire/header.go b/internal/wire/header.go
index 6e8d4f9f..37f48cce 100644
--- a/internal/wire/header.go
+++ b/internal/wire/header.go
@@ -13,8 +13,6 @@ import (
 )
 
 // ParseConnectionID parses the destination connection ID of a packet.
-// It uses the data slice for the connection ID.
-// That means that the connection ID must not be used after the packet buffer is released.
 func ParseConnectionID(data []byte, shortHeaderConnIDLen int) (protocol.ConnectionID, error) {
 	if len(data) == 0 {
 		return protocol.ConnectionID{}, io.EOF
diff --git a/internal/wire/transport_parameters.go b/internal/wire/transport_parameters.go
index 8eb4cf46..dc1f2b95 100644
--- a/internal/wire/transport_parameters.go
+++ b/internal/wire/transport_parameters.go
@@ -118,6 +118,7 @@ func (p *TransportParameters) unmarshal(r *bytes.Reader, sentBy protocol.Perspec
 	var (
 		readOriginalDestinationConnectionID bool
 		readInitialSourceConnectionID       bool
+		readActiveConnectionIDLimit         bool
 	)
 
 	p.AckDelayExponent = protocol.DefaultAckDelayExponent
@@ -139,6 +140,9 @@ func (p *TransportParameters) unmarshal(r *bytes.Reader, sentBy protocol.Perspec
 		}
 		parameterIDs = append(parameterIDs, paramID)
 		switch paramID {
+		case activeConnectionIDLimitParameterID:
+			readActiveConnectionIDLimit = true
+			fallthrough
 		case maxIdleTimeoutParameterID,
 			maxUDPPayloadSizeParameterID,
 			initialMaxDataParameterID,
@@ -148,7 +152,6 @@ func (p *TransportParameters) unmarshal(r *bytes.Reader, sentBy protocol.Perspec
 			initialMaxStreamsBidiParameterID,
 			initialMaxStreamsUniParameterID,
 			maxAckDelayParameterID,
-			activeConnectionIDLimitParameterID,
 			maxDatagramFrameSizeParameterID,
 			ackDelayExponentParameterID:
 			if err := p.readNumericTransportParameter(r, paramID, int(paramLen)); err != nil {
@@ -196,6 +199,9 @@ func (p *TransportParameters) unmarshal(r *bytes.Reader, sentBy protocol.Perspec
 		}
 	}
 
+	if !readActiveConnectionIDLimit {
+		p.ActiveConnectionIDLimit = protocol.DefaultActiveConnectionIDLimit
+	}
 	if !fromSessionTicket {
 		if sentBy == protocol.PerspectiveServer && !readOriginalDestinationConnectionID {
 			return errors.New("missing original_destination_connection_id")
@@ -402,7 +408,9 @@ func (p *TransportParameters) Marshal(pers protocol.Perspective) []byte {
 		}
 	}
 	// active_connection_id_limit
-	b = p.marshalVarintParam(b, activeConnectionIDLimitParameterID, p.ActiveConnectionIDLimit)
+	if p.ActiveConnectionIDLimit != protocol.DefaultActiveConnectionIDLimit {
+		b = p.marshalVarintParam(b, activeConnectionIDLimitParameterID, p.ActiveConnectionIDLimit)
+	}
 	// initial_source_connection_id
 	b = quicvarint.Append(b, uint64(initialSourceConnectionIDParameterID))
 	b = quicvarint.Append(b, uint64(p.InitialSourceConnectionID.Len()))
diff --git a/packet_packer.go b/packet_packer.go
index 14befd46..180a03e1 100644
--- a/packet_packer.go
+++ b/packet_packer.go
@@ -417,7 +417,7 @@ func (p *packetPacker) PackCoalescedPacket(onlyAck bool, v protocol.VersionNumbe
 			if oneRTTPayload.length > 0 {
 				size += p.shortHeaderPacketLength(connID, oneRTTPacketNumberLen, oneRTTPayload) + protocol.ByteCount(oneRTTSealer.Overhead())
 			}
-		} else if p.perspective == protocol.PerspectiveClient { // 0-RTT
+		} else if p.perspective == protocol.PerspectiveClient && !onlyAck { // 0-RTT packets can't contain ACK frames
 			var err error
 			zeroRTTSealer, err = p.cryptoSetup.Get0RTTSealer()
 			if err != nil && err != handshake.ErrKeysDropped && err != handshake.ErrKeysNotYetAvailable {
diff --git a/quicvarint/varint.go b/quicvarint/varint.go
index cbebfe61..3f12c076 100644
--- a/quicvarint/varint.go
+++ b/quicvarint/varint.go
@@ -70,25 +70,6 @@ func Read(r io.ByteReader) (uint64, error) {
 	return uint64(b8) + uint64(b7)<<8 + uint64(b6)<<16 + uint64(b5)<<24 + uint64(b4)<<32 + uint64(b3)<<40 + uint64(b2)<<48 + uint64(b1)<<56, nil
 }
 
-// Write writes i in the QUIC varint format to w.
-// Deprecated: use Append instead.
-func Write(w Writer, i uint64) {
-	if i <= maxVarInt1 {
-		w.WriteByte(uint8(i))
-	} else if i <= maxVarInt2 {
-		w.Write([]byte{uint8(i>>8) | 0x40, uint8(i)})
-	} else if i <= maxVarInt4 {
-		w.Write([]byte{uint8(i>>24) | 0x80, uint8(i >> 16), uint8(i >> 8), uint8(i)})
-	} else if i <= maxVarInt8 {
-		w.Write([]byte{
-			uint8(i>>56) | 0xc0, uint8(i >> 48), uint8(i >> 40), uint8(i >> 32),
-			uint8(i >> 24), uint8(i >> 16), uint8(i >> 8), uint8(i),
-		})
-	} else {
-		panic(fmt.Sprintf("%#x doesn't fit into 62 bits", i))
-	}
-}
-
 // Append appends i in the QUIC varint format.
 func Append(b []byte, i uint64) []byte {
 	if i <= maxVarInt1 {
diff --git a/server.go b/server.go
index f5b04549..4043c58c 100644
--- a/server.go
+++ b/server.go
@@ -98,10 +98,12 @@ type baseServer struct {
 		protocol.VersionNumber,
 	) quicConn
 
-	serverError error
-	errorChan   chan struct{}
-	closed      bool
-	running     chan struct{} // closed as soon as run() returns
+	serverError             error
+	errorChan               chan struct{}
+	closed                  bool
+	running                 chan struct{} // closed as soon as run() returns
+	versionNegotiationQueue chan *receivedPacket
+	invalidTokenQueue       chan *receivedPacket
 
 	connQueue    chan quicConn
 	connQueueLen int32 // to be used as an atomic
@@ -229,26 +231,29 @@ func newServer(
 		return nil, err
 	}
 	s := &baseServer{
-		conn:             conn,
-		tlsConf:          tlsConf,
-		config:           config,
-		tokenGenerator:   tokenGenerator,
-		connIDGenerator:  connIDGenerator,
-		connHandler:      connHandler,
-		connQueue:        make(chan quicConn),
-		errorChan:        make(chan struct{}),
-		running:          make(chan struct{}),
-		receivedPackets:  make(chan *receivedPacket, protocol.MaxServerUnprocessedPackets),
-		newConn:          newConnection,
-		tracer:           tracer,
-		logger:           utils.DefaultLogger.WithPrefix("server"),
-		acceptEarlyConns: acceptEarly,
-		onClose:          onClose,
+		conn:                    conn,
+		tlsConf:                 tlsConf,
+		config:                  config,
+		tokenGenerator:          tokenGenerator,
+		connIDGenerator:         connIDGenerator,
+		connHandler:             connHandler,
+		connQueue:               make(chan quicConn),
+		errorChan:               make(chan struct{}),
+		running:                 make(chan struct{}),
+		receivedPackets:         make(chan *receivedPacket, protocol.MaxServerUnprocessedPackets),
+		versionNegotiationQueue: make(chan *receivedPacket, 4),
+		invalidTokenQueue:       make(chan *receivedPacket, 4),
+		newConn:                 newConnection,
+		tracer:                  tracer,
+		logger:                  utils.DefaultLogger.WithPrefix("server"),
+		acceptEarlyConns:        acceptEarly,
+		onClose:                 onClose,
 	}
 	if acceptEarly {
 		s.zeroRTTQueues = map[protocol.ConnectionID]*zeroRTTQueue{}
 	}
 	go s.run()
+	go s.runSendQueue()
 	s.logger.Debugf("Listening for %s connections on %s", conn.LocalAddr().Network(), conn.LocalAddr().String())
 	return s, nil
 }
@@ -272,6 +277,19 @@ func (s *baseServer) run() {
 	}
 }
 
+func (s *baseServer) runSendQueue() {
+	for {
+		select {
+		case <-s.running:
+			return
+		case p := <-s.versionNegotiationQueue:
+			s.maybeSendVersionNegotiationPacket(p)
+		case p := <-s.invalidTokenQueue:
+			s.maybeSendInvalidToken(p)
+		}
+	}
+}
+
 // Accept returns connections that already completed the handshake.
 // It is only valid if acceptEarlyConns is false.
 func (s *baseServer) Accept(ctx context.Context) (Connection, error) {
@@ -353,27 +371,28 @@ func (s *baseServer) handlePacketImpl(p *receivedPacket) bool /* is the buffer s
 		panic(fmt.Sprintf("misrouted packet: %#v", p.data))
 	}
 	v, err := wire.ParseVersion(p.data)
+	// drop the packet if we failed to parse the protocol version
+	if err != nil {
+		s.logger.Debugf("Dropping a packet with an unknown version")
+		if s.tracer != nil {
+			s.tracer.DroppedPacket(p.remoteAddr, logging.PacketTypeNotDetermined, p.Size(), logging.PacketDropUnexpectedPacket)
+		}
+		return false
+	}
 	// send a Version Negotiation Packet if the client is speaking a different protocol version
-	if err != nil || !protocol.IsSupportedVersion(s.config.Versions, v) {
-		if err != nil || p.Size() < protocol.MinUnknownVersionPacketSize {
-			s.logger.Debugf("Dropping a packet with an unknown version that is too small (%d bytes)", p.Size())
-			if s.tracer != nil {
-				s.tracer.DroppedPacket(p.remoteAddr, logging.PacketTypeNotDetermined, p.Size(), logging.PacketDropUnexpectedPacket)
-			}
+	if !protocol.IsSupportedVersion(s.config.Versions, v) {
+		if s.config.DisableVersionNegotiationPackets {
 			return false
 		}
-		_, src, dest, err := wire.ParseArbitraryLenConnectionIDs(p.data)
-		if err != nil { // should never happen
-			s.logger.Debugf("Dropping a packet with an unknown version for which we failed to parse connection IDs")
+
+		if p.Size() < protocol.MinUnknownVersionPacketSize {
+			s.logger.Debugf("Dropping a packet with an unsupported version number %d that is too small (%d bytes)", v, p.Size())
 			if s.tracer != nil {
 				s.tracer.DroppedPacket(p.remoteAddr, logging.PacketTypeNotDetermined, p.Size(), logging.PacketDropUnexpectedPacket)
 			}
 			return false
 		}
-		if !s.config.DisableVersionNegotiationPackets {
-			go s.sendVersionNegotiationPacket(p.remoteAddr, src, dest, p.info.OOB(), v)
-		}
-		return false
+		return s.enqueueVersionNegotiationPacket(p)
 	}
 
 	if wire.Is0RTTPacket(p.data) {
@@ -557,12 +576,7 @@ func (s *baseServer) handleInitialImpl(p *receivedPacket, hdr *wire.Header) erro
 			// For Retry tokens, we send an INVALID_ERROR if
 			// * the token is too old, or
 			// * the token is invalid, in case of a retry token.
-			go func() {
-				defer p.buffer.Release()
-				if err := s.maybeSendInvalidToken(p, hdr); err != nil {
-					s.logger.Debugf("Error sending INVALID_TOKEN error: %s", err)
-				}
-			}()
+			s.enqueueInvalidToken(p)
 			return nil
 		}
 	}
@@ -730,7 +744,27 @@ func (s *baseServer) sendRetry(remoteAddr net.Addr, hdr *wire.Header, info *pack
 	return err
 }
 
-func (s *baseServer) maybeSendInvalidToken(p *receivedPacket, hdr *wire.Header) error {
+func (s *baseServer) enqueueInvalidToken(p *receivedPacket) {
+	select {
+	case s.invalidTokenQueue <- p:
+	default:
+		// it's fine to drop INVALID_TOKEN packets when we are busy
+		p.buffer.Release()
+	}
+}
+
+func (s *baseServer) maybeSendInvalidToken(p *receivedPacket) {
+	defer p.buffer.Release()
+
+	hdr, _, _, err := wire.ParsePacket(p.data)
+	if err != nil {
+		if s.tracer != nil {
+			s.tracer.DroppedPacket(p.remoteAddr, logging.PacketTypeNotDetermined, p.Size(), logging.PacketDropHeaderParseError)
+		}
+		s.logger.Debugf("Error parsing packet: %s", err)
+		return
+	}
+
 	// Only send INVALID_TOKEN if we can unprotect the packet.
 	// This makes sure that we won't send it for packets that were corrupted.
 	sealer, opener := handshake.NewInitialAEAD(hdr.DestConnectionID, protocol.PerspectiveServer, hdr.Version)
@@ -740,21 +774,21 @@ func (s *baseServer) maybeSendInvalidToken(p *receivedPacket, hdr *wire.Header)
 		if s.tracer != nil {
 			s.tracer.DroppedPacket(p.remoteAddr, logging.PacketTypeInitial, p.Size(), logging.PacketDropHeaderParseError)
 		}
-		// don't return the error here. Just drop the packet.
-		return nil
+		return
 	}
 	hdrLen := extHdr.ParsedLen()
 	if _, err := opener.Open(data[hdrLen:hdrLen], data[hdrLen:], extHdr.PacketNumber, data[:hdrLen]); err != nil {
-		// don't return the error here. Just drop the packet.
 		if s.tracer != nil {
 			s.tracer.DroppedPacket(p.remoteAddr, logging.PacketTypeInitial, p.Size(), logging.PacketDropPayloadDecryptError)
 		}
-		return nil
+		return
 	}
 	if s.logger.Debug() {
 		s.logger.Debugf("Client sent an invalid retry token. Sending INVALID_TOKEN to %s.", p.remoteAddr)
 	}
-	return s.sendError(p.remoteAddr, hdr, sealer, qerr.InvalidToken, p.info)
+	if err := s.sendError(p.remoteAddr, hdr, sealer, qerr.InvalidToken, p.info); err != nil {
+		s.logger.Debugf("Error sending INVALID_TOKEN error: %s", err)
+	}
 }
 
 func (s *baseServer) sendConnectionRefused(remoteAddr net.Addr, hdr *wire.Header, info *packetInfo) error {
@@ -807,14 +841,41 @@ func (s *baseServer) sendError(remoteAddr net.Addr, hdr *wire.Header, sealer han
 	return err
 }
 
-func (s *baseServer) sendVersionNegotiationPacket(remote net.Addr, src, dest protocol.ArbitraryLenConnectionID, oob []byte, v protocol.VersionNumber) {
+func (s *baseServer) enqueueVersionNegotiationPacket(p *receivedPacket) (bufferInUse bool) {
+	select {
+	case s.versionNegotiationQueue <- p:
+		return true
+	default:
+		// it's fine to not send version negotiation packets when we are busy
+	}
+	return false
+}
+
+func (s *baseServer) maybeSendVersionNegotiationPacket(p *receivedPacket) {
+	defer p.buffer.Release()
+
+	v, err := wire.ParseVersion(p.data)
+	if err != nil {
+		s.logger.Debugf("failed to parse version for sending version negotiation packet: %s", err)
+		return
+	}
+
+	_, src, dest, err := wire.ParseArbitraryLenConnectionIDs(p.data)
+	if err != nil { // should never happen
+		s.logger.Debugf("Dropping a packet with an unknown version for which we failed to parse connection IDs")
+		if s.tracer != nil {
+			s.tracer.DroppedPacket(p.remoteAddr, logging.PacketTypeNotDetermined, p.Size(), logging.PacketDropUnexpectedPacket)
+		}
+		return
+	}
+
 	s.logger.Debugf("Client offered version %s, sending Version Negotiation", v)
 
 	data := wire.ComposeVersionNegotiation(dest, src, s.config.Versions)
 	if s.tracer != nil {
-		s.tracer.SentVersionNegotiationPacket(remote, src, dest, s.config.Versions)
+		s.tracer.SentVersionNegotiationPacket(p.remoteAddr, src, dest, s.config.Versions)
 	}
-	if _, err := s.conn.WritePacket(data, remote, oob); err != nil {
+	if _, err := s.conn.WritePacket(data, p.remoteAddr, p.info.OOB()); err != nil {
 		s.logger.Debugf("Error sending Version Negotiation: %s", err)
 	}
 }
diff --git a/sys_conn_buffers.go b/sys_conn_buffers.go
new file mode 100644
index 00000000..0c4e024a
--- /dev/null
+++ b/sys_conn_buffers.go
@@ -0,0 +1,68 @@
+package quic
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"syscall"
+
+	"github.com/quic-go/quic-go/internal/protocol"
+	"github.com/quic-go/quic-go/internal/utils"
+)
+
+//go:generate sh -c "echo '// Code generated by go generate. DO NOT EDIT.\n// Source: sys_conn_buffers.go\n' > sys_conn_buffers_write.go && sed -e 's/SetReadBuffer/SetWriteBuffer/g' -e 's/setReceiveBuffer/setSendBuffer/g' -e 's/inspectReadBuffer/inspectWriteBuffer/g' -e 's/protocol\\.DesiredReceiveBufferSize/protocol\\.DesiredSendBufferSize/g' -e 's/forceSetReceiveBuffer/forceSetSendBuffer/g' -e 's/receive buffer/send buffer/g' sys_conn_buffers.go | sed '/^\\/\\/go:generate/d' >> sys_conn_buffers_write.go"
+func setReceiveBuffer(c net.PacketConn, logger utils.Logger) error {
+	conn, ok := c.(interface{ SetReadBuffer(int) error })
+	if !ok {
+		return errors.New("connection doesn't allow setting of receive buffer size. Not a *net.UDPConn?")
+	}
+
+	var syscallConn syscall.RawConn
+	if sc, ok := c.(interface {
+		SyscallConn() (syscall.RawConn, error)
+	}); ok {
+		var err error
+		syscallConn, err = sc.SyscallConn()
+		if err != nil {
+			syscallConn = nil
+		}
+	}
+	// The connection has a SetReadBuffer method, but we couldn't obtain a syscall.RawConn.
+	// This shouldn't happen for a net.UDPConn, but is possible if the connection just implements the
+	// net.PacketConn interface and the SetReadBuffer method.
+	// We have no way of checking if increasing the buffer size actually worked.
+	if syscallConn == nil {
+		return conn.SetReadBuffer(protocol.DesiredReceiveBufferSize)
+	}
+
+	size, err := inspectReadBuffer(syscallConn)
+	if err != nil {
+		return fmt.Errorf("failed to determine receive buffer size: %w", err)
+	}
+	if size >= protocol.DesiredReceiveBufferSize {
+		logger.Debugf("Conn has receive buffer of %d kiB (wanted: at least %d kiB)", size/1024, protocol.DesiredReceiveBufferSize/1024)
+		return nil
+	}
+	// Ignore the error. We check if we succeeded by querying the buffer size afterward.
+	_ = conn.SetReadBuffer(protocol.DesiredReceiveBufferSize)
+	newSize, err := inspectReadBuffer(syscallConn)
+	if newSize < protocol.DesiredReceiveBufferSize {
+		// Try again with RCVBUFFORCE on Linux
+		_ = forceSetReceiveBuffer(syscallConn, protocol.DesiredReceiveBufferSize)
+		newSize, err = inspectReadBuffer(syscallConn)
+		if err != nil {
+			return fmt.Errorf("failed to determine receive buffer size: %w", err)
+		}
+	}
+	if err != nil {
+		return fmt.Errorf("failed to determine receive buffer size: %w", err)
+	}
+	if newSize == size {
+		return fmt.Errorf("failed to increase receive buffer size (wanted: %d kiB, got %d kiB)", protocol.DesiredReceiveBufferSize/1024, newSize/1024)
+	}
+	if newSize < protocol.DesiredReceiveBufferSize {
+		return fmt.Errorf("failed to sufficiently increase receive buffer size (was: %d kiB, wanted: %d kiB, got: %d kiB)", size/1024, protocol.DesiredReceiveBufferSize/1024, newSize/1024)
+	}
+	logger.Debugf("Increased receive buffer size to %d kiB", newSize/1024)
+	return nil
+}
diff --git a/sys_conn_buffers_write.go b/sys_conn_buffers_write.go
new file mode 100644
index 00000000..86fead96
--- /dev/null
+++ b/sys_conn_buffers_write.go
@@ -0,0 +1,70 @@
+// Code generated by go generate. DO NOT EDIT.
+// Source: sys_conn_buffers.go
+
+package quic
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"syscall"
+
+	"github.com/quic-go/quic-go/internal/protocol"
+	"github.com/quic-go/quic-go/internal/utils"
+)
+
+func setSendBuffer(c net.PacketConn, logger utils.Logger) error {
+	conn, ok := c.(interface{ SetWriteBuffer(int) error })
+	if !ok {
+		return errors.New("connection doesn't allow setting of send buffer size. Not a *net.UDPConn?")
+	}
+
+	var syscallConn syscall.RawConn
+	if sc, ok := c.(interface {
+		SyscallConn() (syscall.RawConn, error)
+	}); ok {
+		var err error
+		syscallConn, err = sc.SyscallConn()
+		if err != nil {
+			syscallConn = nil
+		}
+	}
+	// The connection has a SetWriteBuffer method, but we couldn't obtain a syscall.RawConn.
+	// This shouldn't happen for a net.UDPConn, but is possible if the connection just implements the
+	// net.PacketConn interface and the SetWriteBuffer method.
+	// We have no way of checking if increasing the buffer size actually worked.
+	if syscallConn == nil {
+		return conn.SetWriteBuffer(protocol.DesiredSendBufferSize)
+	}
+
+	size, err := inspectWriteBuffer(syscallConn)
+	if err != nil {
+		return fmt.Errorf("failed to determine send buffer size: %w", err)
+	}
+	if size >= protocol.DesiredSendBufferSize {
+		logger.Debugf("Conn has send buffer of %d kiB (wanted: at least %d kiB)", size/1024, protocol.DesiredSendBufferSize/1024)
+		return nil
+	}
+	// Ignore the error. We check if we succeeded by querying the buffer size afterward.
+	_ = conn.SetWriteBuffer(protocol.DesiredSendBufferSize)
+	newSize, err := inspectWriteBuffer(syscallConn)
+	if newSize < protocol.DesiredSendBufferSize {
+		// Try again with RCVBUFFORCE on Linux
+		_ = forceSetSendBuffer(syscallConn, protocol.DesiredSendBufferSize)
+		newSize, err = inspectWriteBuffer(syscallConn)
+		if err != nil {
+			return fmt.Errorf("failed to determine send buffer size: %w", err)
+		}
+	}
+	if err != nil {
+		return fmt.Errorf("failed to determine send buffer size: %w", err)
+	}
+	if newSize == size {
+		return fmt.Errorf("failed to increase send buffer size (wanted: %d kiB, got %d kiB)", protocol.DesiredSendBufferSize/1024, newSize/1024)
+	}
+	if newSize < protocol.DesiredSendBufferSize {
+		return fmt.Errorf("failed to sufficiently increase send buffer size (was: %d kiB, wanted: %d kiB, got: %d kiB)", size/1024, protocol.DesiredSendBufferSize/1024, newSize/1024)
+	}
+	logger.Debugf("Increased send buffer size to %d kiB", newSize/1024)
+	return nil
+}
diff --git a/sys_conn_helper_linux.go b/sys_conn_helper_linux.go
index 61c3f54b..2c12233c 100644
--- a/sys_conn_helper_linux.go
+++ b/sys_conn_helper_linux.go
@@ -2,7 +2,11 @@
 
 package quic
 
-import "golang.org/x/sys/unix"
+import (
+	"syscall"
+
+	"golang.org/x/sys/unix"
+)
 
 const msgTypeIPTOS = unix.IP_TOS
 
@@ -17,3 +21,23 @@ const (
 )
 
 const batchSize = 8 // needs to smaller than MaxUint8 (otherwise the type of oobConn.readPos has to be changed)
+
+func forceSetReceiveBuffer(c syscall.RawConn, bytes int) error {
+	var serr error
+	if err := c.Control(func(fd uintptr) {
+		serr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RCVBUFFORCE, bytes)
+	}); err != nil {
+		return err
+	}
+	return serr
+}
+
+func forceSetSendBuffer(c syscall.RawConn, bytes int) error {
+	var serr error
+	if err := c.Control(func(fd uintptr) {
+		serr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SNDBUFFORCE, bytes)
+	}); err != nil {
+		return err
+	}
+	return serr
+}
diff --git a/sys_conn_helper_nonlinux.go b/sys_conn_helper_nonlinux.go
new file mode 100644
index 00000000..80b795c3
--- /dev/null
+++ b/sys_conn_helper_nonlinux.go
@@ -0,0 +1,6 @@
+//go:build !linux
+
+package quic
+
+func forceSetReceiveBuffer(c any, bytes int) error { return nil }
+func forceSetSendBuffer(c any, bytes int) error    { return nil }
diff --git a/sys_conn_no_oob.go b/sys_conn_no_oob.go
index 7ab5040a..c5c7f486 100644
--- a/sys_conn_no_oob.go
+++ b/sys_conn_no_oob.go
@@ -8,8 +8,7 @@ func newConn(c net.PacketConn) (rawConn, error) {
 	return &basicConn{PacketConn: c}, nil
 }
 
-func inspectReadBuffer(interface{}) (int, error) {
-	return 0, nil
-}
+func inspectReadBuffer(any) (int, error)  { return 0, nil }
+func inspectWriteBuffer(any) (int, error) { return 0, nil }
 
 func (i *packetInfo) OOB() []byte { return nil }
diff --git a/sys_conn_oob.go b/sys_conn_oob.go
index 806dfb81..42e5f53b 100644
--- a/sys_conn_oob.go
+++ b/sys_conn_oob.go
@@ -5,7 +5,6 @@ package quic
 import (
 	"encoding/binary"
 	"errors"
-	"fmt"
 	"net"
 	"syscall"
 	"time"
@@ -32,20 +31,10 @@ type batchConn interface {
 	ReadBatch(ms []ipv4.Message, flags int) (int, error)
 }
 
-func inspectReadBuffer(c interface{}) (int, error) {
-	conn, ok := c.(interface {
-		SyscallConn() (syscall.RawConn, error)
-	})
-	if !ok {
-		return 0, errors.New("doesn't have a SyscallConn")
-	}
-	rawConn, err := conn.SyscallConn()
-	if err != nil {
-		return 0, fmt.Errorf("couldn't get syscall.RawConn: %w", err)
-	}
+func inspectReadBuffer(c syscall.RawConn) (int, error) {
 	var size int
 	var serr error
-	if err := rawConn.Control(func(fd uintptr) {
+	if err := c.Control(func(fd uintptr) {
 		size, serr = unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RCVBUF)
 	}); err != nil {
 		return 0, err
@@ -53,6 +42,17 @@ func inspectReadBuffer(c interface{}) (int, error) {
 	return size, serr
 }
 
+func inspectWriteBuffer(c syscall.RawConn) (int, error) {
+	var size int
+	var serr error
+	if err := c.Control(func(fd uintptr) {
+		size, serr = unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SNDBUF)
+	}); err != nil {
+		return 0, err
+	}
+	return size, serr
+}
+
 type oobConn struct {
 	OOBCapablePacketConn
 	batchConn batchConn
diff --git a/sys_conn_windows.go b/sys_conn_windows.go
index b003fe94..580d18a5 100644
--- a/sys_conn_windows.go
+++ b/sys_conn_windows.go
@@ -3,9 +3,6 @@
 package quic
 
 import (
-	"errors"
-	"fmt"
-	"net"
 	"syscall"
 
 	"golang.org/x/sys/windows"
@@ -15,20 +12,10 @@ func newConn(c OOBCapablePacketConn) (rawConn, error) {
 	return &basicConn{PacketConn: c}, nil
 }
 
-func inspectReadBuffer(c net.PacketConn) (int, error) {
-	conn, ok := c.(interface {
-		SyscallConn() (syscall.RawConn, error)
-	})
-	if !ok {
-		return 0, errors.New("doesn't have a SyscallConn")
-	}
-	rawConn, err := conn.SyscallConn()
-	if err != nil {
-		return 0, fmt.Errorf("couldn't get syscall.RawConn: %w", err)
-	}
+func inspectReadBuffer(c syscall.RawConn) (int, error) {
 	var size int
 	var serr error
-	if err := rawConn.Control(func(fd uintptr) {
+	if err := c.Control(func(fd uintptr) {
 		size, serr = windows.GetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_RCVBUF)
 	}); err != nil {
 		return 0, err
@@ -36,4 +23,15 @@ func inspectReadBuffer(c net.PacketConn) (int, error) {
 	return size, serr
 }
 
+func inspectWriteBuffer(c syscall.RawConn) (int, error) {
+	var size int
+	var serr error
+	if err := c.Control(func(fd uintptr) {
+		size, serr = windows.GetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_SNDBUF)
+	}); err != nil {
+		return 0, err
+	}
+	return size, serr
+}
+
 func (i *packetInfo) OOB() []byte { return nil }
diff --git a/transport.go b/transport.go
index 153675da..c62ee388 100644
--- a/transport.go
+++ b/transport.go
@@ -5,7 +5,6 @@ import (
 	"crypto/rand"
 	"crypto/tls"
 	"errors"
-	"fmt"
 	"log"
 	"net"
 	"os"
@@ -67,7 +66,8 @@ type Transport struct {
 
 	conn rawConn
 
-	closeQueue chan closePacket
+	closeQueue          chan closePacket
+	statelessResetQueue chan *receivedPacket
 
 	listening   chan struct{} // is closed when listen returns
 	closed      bool
@@ -167,39 +167,6 @@ func (t *Transport) DialEarly(ctx context.Context, addr net.Addr, tlsConf *tls.C
 	return dial(ctx, newSendConn(t.conn, addr, nil), t.connIDGenerator, t.handlerMap, tlsConf, conf, onClose, true)
 }
 
-func setReceiveBuffer(c net.PacketConn, logger utils.Logger) error {
-	conn, ok := c.(interface{ SetReadBuffer(int) error })
-	if !ok {
-		return errors.New("connection doesn't allow setting of receive buffer size. Not a *net.UDPConn?")
-	}
-	size, err := inspectReadBuffer(c)
-	if err != nil {
-		return fmt.Errorf("failed to determine receive buffer size: %w", err)
-	}
-	if size >= protocol.DesiredReceiveBufferSize {
-		logger.Debugf("Conn has receive buffer of %d kiB (wanted: at least %d kiB)", size/1024, protocol.DesiredReceiveBufferSize/1024)
-		return nil
-	}
-	if err := conn.SetReadBuffer(protocol.DesiredReceiveBufferSize); err != nil {
-		return fmt.Errorf("failed to increase receive buffer size: %w", err)
-	}
-	newSize, err := inspectReadBuffer(c)
-	if err != nil {
-		return fmt.Errorf("failed to determine receive buffer size: %w", err)
-	}
-	if newSize == size {
-		return fmt.Errorf("failed to increase receive buffer size (wanted: %d kiB, got %d kiB)", protocol.DesiredReceiveBufferSize/1024, newSize/1024)
-	}
-	if newSize < protocol.DesiredReceiveBufferSize {
-		return fmt.Errorf("failed to sufficiently increase receive buffer size (was: %d kiB, wanted: %d kiB, got: %d kiB)", size/1024, protocol.DesiredReceiveBufferSize/1024, newSize/1024)
-	}
-	logger.Debugf("Increased receive buffer size to %d kiB", newSize/1024)
-	return nil
-}
-
-// only print warnings about the UDP receive buffer size once
-var receiveBufferWarningOnce sync.Once
-
 func (t *Transport) init(isServer bool) error {
 	t.initOnce.Do(func() {
 		getMultiplexer().AddConn(t.Conn)
@@ -216,6 +183,7 @@ func (t *Transport) init(isServer bool) error {
 		t.listening = make(chan struct{})
 
 		t.closeQueue = make(chan closePacket, 4)
+		t.statelessResetQueue = make(chan *receivedPacket, 4)
 
 		if t.ConnectionIDGenerator != nil {
 			t.connIDGenerator = t.ConnectionIDGenerator
@@ -230,7 +198,7 @@ func (t *Transport) init(isServer bool) error {
 		}
 
 		go t.listen(conn)
-		go t.runCloseQueue()
+		go t.runSendQueue()
 	})
 	return t.initErr
 }
@@ -244,13 +212,15 @@ func (t *Transport) enqueueClosePacket(p closePacket) {
 	}
 }
 
-func (t *Transport) runCloseQueue() {
+func (t *Transport) runSendQueue() {
 	for {
 		select {
 		case <-t.listening:
 			return
 		case p := <-t.closeQueue:
 			t.conn.WritePacket(p.payload, p.addr, p.info.OOB())
+		case p := <-t.statelessResetQueue:
+			t.sendStatelessReset(p)
 		}
 	}
 }
@@ -303,13 +273,26 @@ func (t *Transport) close(e error) {
 	t.closed = true
 }
 
+// only print warnings about the UDP receive buffer size once
+var setBufferWarningOnce sync.Once
+
 func (t *Transport) listen(conn rawConn) {
 	defer close(t.listening)
 	defer getMultiplexer().RemoveConn(t.Conn)
 
 	if err := setReceiveBuffer(t.Conn, t.logger); err != nil {
 		if !strings.Contains(err.Error(), "use of closed network connection") {
-			receiveBufferWarningOnce.Do(func() {
+			setBufferWarningOnce.Do(func() {
+				if disable, _ := strconv.ParseBool(os.Getenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING")); disable {
+					return
+				}
+				log.Printf("%s. See https://github.com/quic-go/quic-go/wiki/UDP-Receive-Buffer-Size for details.", err)
+			})
+		}
+	}
+	if err := setSendBuffer(t.Conn, t.logger); err != nil {
+		if !strings.Contains(err.Error(), "use of closed network connection") {
+			setBufferWarningOnce.Do(func() {
 				if disable, _ := strconv.ParseBool(os.Getenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING")); disable {
 					return
 				}
@@ -361,7 +344,7 @@ func (t *Transport) handlePacket(p *receivedPacket) {
 		return
 	}
 	if !wire.IsLongHeaderPacket(p.data[0]) {
-		go t.maybeSendStatelessReset(p, connID)
+		t.maybeSendStatelessReset(p)
 		return
 	}
 
@@ -374,14 +357,33 @@ func (t *Transport) handlePacket(p *receivedPacket) {
 	t.server.handlePacket(p)
 }
 
-func (t *Transport) maybeSendStatelessReset(p *receivedPacket, connID protocol.ConnectionID) {
-	defer p.buffer.Release()
+func (t *Transport) maybeSendStatelessReset(p *receivedPacket) {
 	if t.StatelessResetKey == nil {
+		p.buffer.Release()
 		return
 	}
+
 	// Don't send a stateless reset in response to very small packets.
 	// This includes packets that could be stateless resets.
 	if len(p.data) <= protocol.MinStatelessResetSize {
+		p.buffer.Release()
+		return
+	}
+
+	select {
+	case t.statelessResetQueue <- p:
+	default:
+		// it's fine to not send a stateless reset when we're busy
+		p.buffer.Release()
+	}
+}
+
+func (t *Transport) sendStatelessReset(p *receivedPacket) {
+	defer p.buffer.Release()
+
+	connID, err := wire.ParseConnectionID(p.data, t.connIDLen)
+	if err != nil {
+		t.logger.Errorf("error parsing connection ID on packet from %s: %s", p.remoteAddr, err)
 		return
 	}
 	token := t.handlerMap.GetStatelessResetToken(connID)
@@ -391,7 +393,7 @@ func (t *Transport) maybeSendStatelessReset(p *receivedPacket, connID protocol.C
 	data[0] = (data[0] & 0x7f) | 0x40
 	data = append(data, token[:]...)
 	if _, err := t.conn.WritePacket(data, p.remoteAddr, p.info.OOB()); err != nil {
-		t.logger.Debugf("Error sending Stateless Reset: %s", err)
+		t.logger.Debugf("Error sending Stateless Reset to %s: %s", p.remoteAddr, err)
 	}
 }
 


Test patch:
diff --git a/client_test.go b/client_test.go
index 23847329..50346891 100644
--- a/client_test.go
+++ b/client_test.go
@@ -293,7 +293,7 @@ var _ = Describe("Client", func() {
 				conn := NewMockQUICConn(mockCtrl)
 				conn.EXPECT().run()
 				conn.EXPECT().HandshakeComplete().Return(make(chan struct{}))
-				conn.EXPECT().destroy(gomock.Any())
+				conn.EXPECT().destroy(gomock.Any()).MaxTimes(1)
 				close(done)
 				return conn
 			}
diff --git a/http3/client_test.go b/http3/client_test.go
index d0f68295..2bc00884 100644
--- a/http3/client_test.go
+++ b/http3/client_test.go
@@ -90,6 +90,22 @@ var _ = Describe("Client", func() {
 		Expect(dialAddrCalled).To(BeTrue())
 	})
 
+	It("sets the ServerName in the tls.Config, if not set", func() {
+		const host = "foo.bar"
+		dialCalled := false
+		dialFunc := func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
+			Expect(tlsCfg.ServerName).To(Equal(host))
+			dialCalled = true
+			return nil, errors.New("test done")
+		}
+		client, err := newClient(host, nil, &roundTripperOpts{}, nil, dialFunc)
+		Expect(err).ToNot(HaveOccurred())
+		req, err := http.NewRequest("GET", "https://foo.bar", nil)
+		Expect(err).ToNot(HaveOccurred())
+		client.RoundTripOpt(req, RoundTripOpt{})
+		Expect(dialCalled).To(BeTrue())
+	})
+
 	It("uses the TLS config and QUIC config", func() {
 		tlsConf := &tls.Config{
 			ServerName: "foo.bar",
diff --git a/http3/roundtrip_test.go b/http3/roundtrip_test.go
index 14388b5f..0c219db7 100644
--- a/http3/roundtrip_test.go
+++ b/http3/roundtrip_test.go
@@ -6,12 +6,11 @@ import (
 	"crypto/tls"
 	"errors"
 	"io"
-	"net"
 	"net/http"
+	"sync/atomic"
 	"time"
 
 	"github.com/quic-go/quic-go"
-	mockquic "github.com/quic-go/quic-go/internal/mocks/quic"
 	"github.com/quic-go/quic-go/internal/qerr"
 
 	"github.com/golang/mock/gomock"
@@ -304,10 +303,10 @@ var _ = Describe("RoundTripper", func() {
 
 	Context("closing", func() {
 		It("closes", func() {
-			rt.clients = make(map[string]roundTripCloser)
+			rt.clients = make(map[string]*roundTripCloserWithCount)
 			cl := NewMockRoundTripCloser(mockCtrl)
 			cl.EXPECT().Close()
-			rt.clients["foo.bar"] = cl
+			rt.clients["foo.bar"] = &roundTripCloserWithCount{cl, atomic.Int64{}}
 			err := rt.Close()
 			Expect(err).ToNot(HaveOccurred())
 			Expect(len(rt.clients)).To(BeZero())
@@ -319,67 +318,52 @@ var _ = Describe("RoundTripper", func() {
 			Expect(err).ToNot(HaveOccurred())
 			Expect(len(rt.clients)).To(BeZero())
 		})
-	})
-
-	Context("reusing udpconn", func() {
-		var originalDialer func(ctx context.Context, pconn net.PacketConn, remoteAddr net.Addr, tlsConf *tls.Config, config *quic.Config) (quic.EarlyConnection, error)
-		var req1, req2 *http.Request
 
-		BeforeEach(func() {
-			var err error
-			originalDialer = quicDialer
-			req1, err = http.NewRequest("GET", "https://site1.com", nil)
+		It("closes idle connections", func() {
+			Expect(len(rt.clients)).To(Equal(0))
+			req1, err := http.NewRequest("GET", "https://site1.com", nil)
 			Expect(err).ToNot(HaveOccurred())
-			req2, err = http.NewRequest("GET", "https://site2.com", nil)
+			req2, err := http.NewRequest("GET", "https://site2.com", nil)
 			Expect(err).ToNot(HaveOccurred())
 			Expect(req1.Host).ToNot(Equal(req2.Host))
-		})
-
-		AfterEach(func() {
-			quicDialer = originalDialer
-			err := rt.Close()
-			Expect(err).ToNot(HaveOccurred())
-		})
-
-		It("creates udpconn at first request", func() {
-			Expect(rt.udpConn).To(BeNil())
+			ctx1, cancel1 := context.WithCancel(context.Background())
+			ctx2, cancel2 := context.WithCancel(context.Background())
+			req1 = req1.WithContext(ctx1)
+			req2 = req2.WithContext(ctx2)
+			roundTripCalled := make(chan struct{})
+			reqFinished := make(chan struct{})
 			rt.newClient = func(hostname string, tlsConf *tls.Config, opts *roundTripperOpts, conf *quic.Config, dialer dialFunc) (roundTripCloser, error) {
 				cl := NewMockRoundTripCloser(mockCtrl)
-				cl.EXPECT().RoundTripOpt(gomock.Any(), gomock.Any())
 				cl.EXPECT().Close()
-				return cl, nil
-			}
-			_, err := rt.RoundTrip(req1)
-			Expect(err).ToNot(HaveOccurred())
-			Expect(rt.udpConn).ToNot(BeNil())
-		})
-
-		It("reuses udpconn in different hosts", func() {
-			Expect(rt.udpConn).To(BeNil())
-			quicDialer = func(_ context.Context, pconn net.PacketConn, _ net.Addr, _ *tls.Config, _ *quic.Config) (quic.EarlyConnection, error) {
-				conn := mockquic.NewMockEarlyConnection(mockCtrl)
-				conn.EXPECT().LocalAddr().Return(pconn.LocalAddr())
-				return conn, nil
-			}
-			rt.newClient = func(hostname string, tlsConf *tls.Config, _ *roundTripperOpts, conf *quic.Config, dialer dialFunc) (roundTripCloser, error) {
-				cl := NewMockRoundTripCloser(mockCtrl)
-				cl.EXPECT().RoundTripOpt(gomock.Any(), gomock.Any()).DoAndReturn(func(_ *http.Request, _ RoundTripOpt) (*http.Response, error) {
-					header := make(http.Header)
-					quicConn, err := dialer(context.Background(), hostname, tlsConf, conf)
-					Expect(err).ToNot(HaveOccurred())
-					header.Set("udpconn", quicConn.LocalAddr().String())
-					return &http.Response{Header: header}, nil
+				cl.EXPECT().RoundTripOpt(gomock.Any(), gomock.Any()).DoAndReturn(func(r *http.Request, _ RoundTripOpt) (*http.Response, error) {
+					roundTripCalled <- struct{}{}
+					<-r.Context().Done()
+					return nil, nil
 				})
-				cl.EXPECT().Close()
 				return cl, nil
 			}
-			rsp1, err := rt.RoundTrip(req1)
-			Expect(err).ToNot(HaveOccurred())
-			Expect(rsp1.Header.Get("udpconn")).ToNot(Equal(""))
-			rsp2, err := rt.RoundTrip(req2)
-			Expect(err).ToNot(HaveOccurred())
-			Expect(rsp2.Header.Get("udpconn")).ToNot(Equal(""))
-			Expect(rsp1.Header.Get("udpconn")).To(Equal(rsp2.Header.Get("udpconn")))
+			go func() {
+				rt.RoundTrip(req1)
+				reqFinished <- struct{}{}
+			}()
+			go func() {
+				rt.RoundTrip(req2)
+				reqFinished <- struct{}{}
+			}()
+			<-roundTripCalled
+			<-roundTripCalled
+			// Both two requests are started.
+			Expect(len(rt.clients)).To(Equal(2))
+			cancel1()
+			<-reqFinished
+			// req1 is finished
+			rt.CloseIdleConnections()
+			Expect(len(rt.clients)).To(Equal(1))
+			cancel2()
+			<-reqFinished
+			// all requests are finished
+			rt.CloseIdleConnections()
+			Expect(len(rt.clients)).To(Equal(0))
 		})
 	})
 })
diff --git a/integrationtests/gomodvendor/go.sum b/integrationtests/gomodvendor/go.sum
index 9ba8661b..c7b64468 100644
--- a/integrationtests/gomodvendor/go.sum
+++ b/integrationtests/gomodvendor/go.sum
@@ -28,8 +28,12 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
-github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
+github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
@@ -46,16 +50,18 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@@ -93,14 +99,33 @@ github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
 github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
 github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
 github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
-github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
+github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
 github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
+github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0=
+github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo=
+github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw=
+github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo=
+github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc=
+github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk=
+github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo=
+github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
+github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
+github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
 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.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
-github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
 github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
+github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
+github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
+github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
+github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
+github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
+github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw=
+github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw=
+github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
+github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
+github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
 github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -143,8 +168,9 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh
 github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
 github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
 github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
@@ -175,8 +201,12 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
+golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
+golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -197,9 +227,14 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -214,6 +249,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -239,14 +275,22 @@ golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -254,8 +298,11 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -268,8 +315,12 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
 golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
+golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
+golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
+golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -309,8 +360,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
diff --git a/integrationtests/self/datagram_test.go b/integrationtests/self/datagram_test.go
index 2ccbbe16..65cb13fa 100644
--- a/integrationtests/self/datagram_test.go
+++ b/integrationtests/self/datagram_test.go
@@ -119,10 +119,7 @@ var _ = Describe("Datagram test", func() {
 		var counter int
 		for {
 			// Close the connection if no message is received for 100 ms.
-			timer := time.AfterFunc(scaleDuration(100*time.Millisecond), func() {
-				fmt.Println("closing conn")
-				conn.CloseWithError(0, "")
-			})
+			timer := time.AfterFunc(scaleDuration(100*time.Millisecond), func() { conn.CloseWithError(0, "") })
 			if _, err := conn.ReceiveMessage(); err != nil {
 				break
 			}
diff --git a/integrationtests/self/hotswap_test.go b/integrationtests/self/hotswap_test.go
index 6cd73079..ac75a494 100644
--- a/integrationtests/self/hotswap_test.go
+++ b/integrationtests/self/hotswap_test.go
@@ -61,6 +61,7 @@ var _ = Describe("HTTP3 Server hotswap test", func() {
 		mux1    *http.ServeMux
 		mux2    *http.ServeMux
 		client  *http.Client
+		rt      *http3.RoundTripper
 		server1 *http3.Server
 		server2 *http3.Server
 		ln      *listenerWrapper
@@ -97,17 +98,17 @@ var _ = Describe("HTTP3 Server hotswap test", func() {
 	})
 
 	AfterEach(func() {
+		Expect(rt.Close()).NotTo(HaveOccurred())
 		Expect(ln.Close()).NotTo(HaveOccurred())
 	})
 
 	BeforeEach(func() {
-		client = &http.Client{
-			Transport: &http3.RoundTripper{
-				TLSClientConfig:    getTLSClientConfig(),
-				DisableCompression: true,
-				QuicConfig:         getQuicConfig(&quic.Config{MaxIdleTimeout: 10 * time.Second}),
-			},
+		rt = &http3.RoundTripper{
+			TLSClientConfig:    getTLSClientConfig(),
+			DisableCompression: true,
+			QuicConfig:         getQuicConfig(&quic.Config{MaxIdleTimeout: 10 * time.Second}),
 		}
+		client = &http.Client{Transport: rt}
 	})
 
 	It("hotswap works", func() {
diff --git a/integrationtests/self/http_test.go b/integrationtests/self/http_test.go
index 918bfdf2..f8d563b9 100644
--- a/integrationtests/self/http_test.go
+++ b/integrationtests/self/http_test.go
@@ -40,6 +40,7 @@ var _ = Describe("HTTP tests", func() {
 	var (
 		mux            *http.ServeMux
 		client         *http.Client
+		rt             *http3.RoundTripper
 		server         *http3.Server
 		stoppedServing chan struct{}
 		port           string
@@ -77,6 +78,12 @@ var _ = Describe("HTTP tests", func() {
 			w.Write(body) // don't check the error here. Stream may be reset.
 		})
 
+		mux.HandleFunc("/remoteAddr", func(w http.ResponseWriter, r *http.Request) {
+			defer GinkgoRecover()
+			w.Header().Set("X-RemoteAddr", r.RemoteAddr)
+			w.WriteHeader(http.StatusOK)
+		})
+
 		server = &http3.Server{
 			Handler:    mux,
 			TLSConfig:  getTLSConfig(),
@@ -99,18 +106,18 @@ var _ = Describe("HTTP tests", func() {
 	})
 
 	AfterEach(func() {
+		Expect(rt.Close()).NotTo(HaveOccurred())
 		Expect(server.Close()).NotTo(HaveOccurred())
 		Eventually(stoppedServing).Should(BeClosed())
 	})
 
 	BeforeEach(func() {
-		client = &http.Client{
-			Transport: &http3.RoundTripper{
-				TLSClientConfig:    getTLSClientConfig(),
-				DisableCompression: true,
-				QuicConfig:         getQuicConfig(&quic.Config{MaxIdleTimeout: 10 * time.Second}),
-			},
+		rt = &http3.RoundTripper{
+			TLSClientConfig:    getTLSClientConfigWithoutServerName(),
+			DisableCompression: true,
+			QuicConfig:         getQuicConfig(&quic.Config{MaxIdleTimeout: 10 * time.Second}),
 		}
+		client = &http.Client{Transport: rt}
 	})
 
 	It("downloads a hello", func() {
@@ -122,6 +129,20 @@ var _ = Describe("HTTP tests", func() {
 		Expect(string(body)).To(Equal("Hello, World!\n"))
 	})
 
+	It("requests to different servers with the same udpconn", func() {
+		resp, err := client.Get("https://localhost:" + port + "/remoteAddr")
+		Expect(err).ToNot(HaveOccurred())
+		Expect(resp.StatusCode).To(Equal(200))
+		addr1 := resp.Header.Get("X-RemoteAddr")
+		Expect(addr1).ToNot(Equal(""))
+		resp, err = client.Get("https://127.0.0.1:" + port + "/remoteAddr")
+		Expect(err).ToNot(HaveOccurred())
+		Expect(resp.StatusCode).To(Equal(200))
+		addr2 := resp.Header.Get("X-RemoteAddr")
+		Expect(addr2).ToNot(Equal(""))
+		Expect(addr1).To(Equal(addr2))
+	})
+
 	It("downloads concurrently", func() {
 		group, ctx := errgroup.WithContext(context.Background())
 		for i := 0; i < 2; i++ {
@@ -375,11 +396,12 @@ var _ = Describe("HTTP tests", func() {
 	})
 
 	It("serves other QUIC connections", func() {
+		tlsConf := getTLSConfig()
 		if version == protocol.VersionDraft29 {
-			Skip("This test only works on RFC versions")
+			tlsConf.NextProtos = []string{http3.NextProtoH3Draft29}
+		} else {
+			tlsConf.NextProtos = []string{http3.NextProtoH3}
 		}
-		tlsConf := getTLSConfig()
-		tlsConf.NextProtos = []string{"h3"}
 		ln, err := quic.ListenAddr("localhost:0", tlsConf, nil)
 		Expect(err).ToNot(HaveOccurred())
 		defer ln.Close()
diff --git a/integrationtests/self/multiplex_test.go b/integrationtests/self/multiplex_test.go
index 2623e322..9b00bc34 100644
--- a/integrationtests/self/multiplex_test.go
+++ b/integrationtests/self/multiplex_test.go
@@ -156,58 +156,58 @@ var _ = Describe("Multiplexing", func() {
 			Eventually(done, timeout).Should(BeClosed())
 		})
 
-		It("runs a server and client on the same conn", func() {
-			if runtime.GOOS == "linux" {
-				Skip("This test would require setting of iptables rules, see https://stackoverflow.com/questions/23859164/linux-udp-socket-sendto-operation-not-permitted.")
-			}
-			addr1, err := net.ResolveUDPAddr("udp", "localhost:0")
-			Expect(err).ToNot(HaveOccurred())
-			conn1, err := net.ListenUDP("udp", addr1)
-			Expect(err).ToNot(HaveOccurred())
-			defer conn1.Close()
-			tr1 := &quic.Transport{Conn: conn1}
-
-			addr2, err := net.ResolveUDPAddr("udp", "localhost:0")
-			Expect(err).ToNot(HaveOccurred())
-			conn2, err := net.ListenUDP("udp", addr2)
-			Expect(err).ToNot(HaveOccurred())
-			defer conn2.Close()
-			tr2 := &quic.Transport{Conn: conn2}
-
-			server1, err := tr1.Listen(
-				getTLSConfig(),
-				getQuicConfig(nil),
-			)
-			Expect(err).ToNot(HaveOccurred())
-			runServer(server1)
-			defer server1.Close()
-
-			server2, err := tr2.Listen(
-				getTLSConfig(),
-				getQuicConfig(nil),
-			)
-			Expect(err).ToNot(HaveOccurred())
-			runServer(server2)
-			defer server2.Close()
-
-			done1 := make(chan struct{})
-			done2 := make(chan struct{})
-			go func() {
-				defer GinkgoRecover()
-				dial(tr2, server1.Addr())
-				close(done1)
-			}()
-			go func() {
-				defer GinkgoRecover()
-				dial(tr1, server2.Addr())
-				close(done2)
-			}()
-			timeout := 30 * time.Second
-			if debugLog() {
-				timeout = time.Minute
-			}
-			Eventually(done1, timeout).Should(BeClosed())
-			Eventually(done2, timeout).Should(BeClosed())
-		})
+		// This test would require setting of iptables rules, see https://stackoverflow.com/questions/23859164/linux-udp-socket-sendto-operation-not-permitted.
+		if runtime.GOOS != "linux" {
+			It("runs a server and client on the same conn", func() {
+				addr1, err := net.ResolveUDPAddr("udp", "localhost:0")
+				Expect(err).ToNot(HaveOccurred())
+				conn1, err := net.ListenUDP("udp", addr1)
+				Expect(err).ToNot(HaveOccurred())
+				defer conn1.Close()
+				tr1 := &quic.Transport{Conn: conn1}
+
+				addr2, err := net.ResolveUDPAddr("udp", "localhost:0")
+				Expect(err).ToNot(HaveOccurred())
+				conn2, err := net.ListenUDP("udp", addr2)
+				Expect(err).ToNot(HaveOccurred())
+				defer conn2.Close()
+				tr2 := &quic.Transport{Conn: conn2}
+
+				server1, err := tr1.Listen(
+					getTLSConfig(),
+					getQuicConfig(nil),
+				)
+				Expect(err).ToNot(HaveOccurred())
+				runServer(server1)
+				defer server1.Close()
+
+				server2, err := tr2.Listen(
+					getTLSConfig(),
+					getQuicConfig(nil),
+				)
+				Expect(err).ToNot(HaveOccurred())
+				runServer(server2)
+				defer server2.Close()
+
+				done1 := make(chan struct{})
+				done2 := make(chan struct{})
+				go func() {
+					defer GinkgoRecover()
+					dial(tr2, server1.Addr())
+					close(done1)
+				}()
+				go func() {
+					defer GinkgoRecover()
+					dial(tr1, server2.Addr())
+					close(done2)
+				}()
+				timeout := 30 * time.Second
+				if debugLog() {
+					timeout = time.Minute
+				}
+				Eventually(done1, timeout).Should(BeClosed())
+				Eventually(done2, timeout).Should(BeClosed())
+			})
+		}
 	})
 })
diff --git a/integrationtests/self/self_suite_test.go b/integrationtests/self/self_suite_test.go
index 10adc856..8f9436e6 100644
--- a/integrationtests/self/self_suite_test.go
+++ b/integrationtests/self/self_suite_test.go
@@ -90,10 +90,11 @@ var (
 	qlogTracer func(context.Context, logging.Perspective, quic.ConnectionID) logging.ConnectionTracer
 	enableQlog bool
 
-	version            quic.VersionNumber
-	tlsConfig          *tls.Config
-	tlsConfigLongChain *tls.Config
-	tlsClientConfig    *tls.Config
+	version                          quic.VersionNumber
+	tlsConfig                        *tls.Config
+	tlsConfigLongChain               *tls.Config
+	tlsClientConfig                  *tls.Config
+	tlsClientConfigWithoutServerName *tls.Config
 )
 
 // read the logfile command line flag
@@ -131,6 +132,10 @@ func init() {
 		RootCAs:    root,
 		NextProtos: []string{alpn},
 	}
+	tlsClientConfigWithoutServerName = &tls.Config{
+		RootCAs:    root,
+		NextProtos: []string{alpn},
+	}
 }
 
 var _ = BeforeSuite(func() {
@@ -165,6 +170,10 @@ func getTLSClientConfig() *tls.Config {
 	return tlsClientConfig.Clone()
 }
 
+func getTLSClientConfigWithoutServerName() *tls.Config {
+	return tlsClientConfigWithoutServerName.Clone()
+}
+
 func getQuicConfig(conf *quic.Config) *quic.Config {
 	if conf == nil {
 		conf = &quic.Config{}
diff --git a/integrationtests/self/timeout_test.go b/integrationtests/self/timeout_test.go
index 5a01270d..358ff1e2 100644
--- a/integrationtests/self/timeout_test.go
+++ b/integrationtests/self/timeout_test.go
@@ -455,6 +455,7 @@ var _ = Describe("Timeout tests", func() {
 			case serverErr := <-serverErrChan:
 				Expect(serverErr).To(HaveOccurred())
 				Expect(serverErr.Error()).To(ContainSubstring(io.ErrClosedPipe.Error()))
+				defer ln.Close()
 			default:
 				Expect(ln.Close()).To(Succeed())
 				Eventually(serverErrChan).Should(Receive())
diff --git a/integrationtests/tools/crypto.go b/integrationtests/tools/crypto.go
index d0bdfb41..6bd02ee0 100644
--- a/integrationtests/tools/crypto.go
+++ b/integrationtests/tools/crypto.go
@@ -9,6 +9,7 @@ import (
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"math/big"
+	"net"
 	"time"
 )
 
@@ -44,6 +45,7 @@ func GenerateLeafCert(ca *x509.Certificate, caPriv crypto.PrivateKey) (*x509.Cer
 	certTempl := &x509.Certificate{
 		SerialNumber: big.NewInt(1),
 		DNSNames:     []string{"localhost"},
+		IPAddresses:  []net.IP{net.IPv4(127, 0, 0, 1)},
 		NotBefore:    time.Now(),
 		NotAfter:     time.Now().Add(24 * time.Hour),
 		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
diff --git a/integrationtests/tools/proxy/proxy.go b/integrationtests/tools/proxy/proxy.go
index 01e6a4a9..4ed2d89d 100644
--- a/integrationtests/tools/proxy/proxy.go
+++ b/integrationtests/tools/proxy/proxy.go
@@ -169,6 +169,12 @@ func NewQuicProxy(local string, opts *Opts) (*QuicProxy, error) {
 	if err != nil {
 		return nil, err
 	}
+	if err := conn.SetReadBuffer(protocol.DesiredReceiveBufferSize); err != nil {
+		return nil, err
+	}
+	if err := conn.SetWriteBuffer(protocol.DesiredSendBufferSize); err != nil {
+		return nil, err
+	}
 	raddr, err := net.ResolveUDPAddr("udp", opts.RemoteAddr)
 	if err != nil {
 		return nil, err
@@ -225,13 +231,19 @@ func (p *QuicProxy) LocalPort() int {
 }
 
 func (p *QuicProxy) newConnection(cliAddr *net.UDPAddr) (*connection, error) {
-	srvudp, err := net.DialUDP("udp", nil, p.serverAddr)
+	conn, err := net.DialUDP("udp", nil, p.serverAddr)
 	if err != nil {
 		return nil, err
 	}
+	if err := conn.SetReadBuffer(protocol.DesiredReceiveBufferSize); err != nil {
+		return nil, err
+	}
+	if err := conn.SetWriteBuffer(protocol.DesiredSendBufferSize); err != nil {
+		return nil, err
+	}
 	return &connection{
 		ClientAddr:      cliAddr,
-		ServerConn:      srvudp,
+		ServerConn:      conn,
 		incomingPackets: make(chan packetEntry, 10),
 		Incoming:        newQueue(),
 		Outgoing:        newQueue(),
diff --git a/internal/utils/ringbuffer/ringbuffer_bench_test.go b/internal/utils/ringbuffer/ringbuffer_bench_test.go
new file mode 100644
index 00000000..ac92ed33
--- /dev/null
+++ b/internal/utils/ringbuffer/ringbuffer_bench_test.go
@@ -0,0 +1,12 @@
+package ringbuffer
+
+import "testing"
+
+func BenchmarkRingBuffer(b *testing.B) {
+	r := RingBuffer[int]{}
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		r.PushBack(i)
+		r.PopFront()
+	}
+}
diff --git a/internal/utils/ringbuffer/ringbuffer_suite_test.go b/internal/utils/ringbuffer/ringbuffer_suite_test.go
new file mode 100644
index 00000000..c537d459
--- /dev/null
+++ b/internal/utils/ringbuffer/ringbuffer_suite_test.go
@@ -0,0 +1,13 @@
+package ringbuffer
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestTestdata(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "ringbuffer suite")
+}
diff --git a/internal/utils/ringbuffer/ringbuffer_test.go b/internal/utils/ringbuffer/ringbuffer_test.go
new file mode 100644
index 00000000..13241a30
--- /dev/null
+++ b/internal/utils/ringbuffer/ringbuffer_test.go
@@ -0,0 +1,38 @@
+package ringbuffer
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("RingBuffer", func() {
+	It("push and pop", func() {
+		r := RingBuffer[int]{}
+		Expect(len(r.ring)).To(Equal(0))
+		Expect(func() { r.PopFront() }).To(Panic())
+		r.PushBack(1)
+		r.PushBack(2)
+		r.PushBack(3)
+		Expect(r.PopFront()).To(Equal(1))
+		Expect(r.PopFront()).To(Equal(2))
+		r.PushBack(4)
+		r.PushBack(5)
+		Expect(r.Len()).To(Equal(3))
+		r.PushBack(6)
+		Expect(r.Len()).To(Equal(4))
+		Expect(r.PopFront()).To(Equal(3))
+		Expect(r.PopFront()).To(Equal(4))
+		Expect(r.PopFront()).To(Equal(5))
+		Expect(r.PopFront()).To(Equal(6))
+	})
+	It("clear", func() {
+		r := RingBuffer[int]{}
+		r.Init(2)
+		r.PushBack(1)
+		r.PushBack(2)
+		Expect(r.full).To(BeTrue())
+		r.Clear()
+		Expect(r.full).To(BeFalse())
+		Expect(r.Len()).To(Equal(0))
+	})
+})
diff --git a/internal/utils/rtt_stats_test.go b/internal/utils/rtt_stats_test.go
index 29892c6f..eae31926 100644
--- a/internal/utils/rtt_stats_test.go
+++ b/internal/utils/rtt_stats_test.go
@@ -154,4 +154,14 @@ var _ = Describe("RTT stats", func() {
 		Expect(rttStats.SmoothedRTT()).To(Equal(200 * time.Millisecond))
 		Expect(rttStats.MeanDeviation()).To(Equal(100 * time.Millisecond))
 	})
+
+	It("doesn't restore the RTT if we already have a measurement", func() {
+		const rtt = 10 * time.Millisecond
+		rttStats.UpdateRTT(rtt, 0, time.Now())
+		Expect(rttStats.LatestRTT()).To(Equal(rtt))
+		Expect(rttStats.SmoothedRTT()).To(Equal(rtt))
+		rttStats.SetInitialRTT(time.Minute)
+		Expect(rttStats.LatestRTT()).To(Equal(rtt))
+		Expect(rttStats.SmoothedRTT()).To(Equal(rtt))
+	})
 })
diff --git a/internal/wire/ack_frame_test.go b/internal/wire/ack_frame_test.go
index 49d11374..f80335c0 100644
--- a/internal/wire/ack_frame_test.go
+++ b/internal/wire/ack_frame_test.go
@@ -170,6 +170,9 @@ var _ = Describe("ACK Frame (for IETF QUIC)", func() {
 				Expect(frame.LargestAcked()).To(Equal(protocol.PacketNumber(100)))
 				Expect(frame.LowestAcked()).To(Equal(protocol.PacketNumber(90)))
 				Expect(frame.HasMissingRanges()).To(BeFalse())
+				Expect(frame.ECT0).To(BeEquivalentTo(0x42))
+				Expect(frame.ECT1).To(BeEquivalentTo(0x12345))
+				Expect(frame.ECNCE).To(BeEquivalentTo(0x12345678))
 				Expect(b.Len()).To(BeZero())
 			})
 
diff --git a/internal/wire/transport_parameter_test.go b/internal/wire/transport_parameter_test.go
index f4c39603..5526750e 100644
--- a/internal/wire/transport_parameter_test.go
+++ b/internal/wire/transport_parameter_test.go
@@ -289,15 +289,16 @@ var _ = Describe("Transport Parameters", func() {
 		Expect(float32(dataLen) / num).To(BeNumerically("~", float32(defaultLen)/num+float32(entryLen), 1))
 	})
 
-	It("sets the default value for the ack_delay_exponent, when no value was sent", func() {
+	It("sets the default value for the ack_delay_exponent and max_active_connection_id_limit, when no values were sent", func() {
 		data := (&TransportParameters{
 			AckDelayExponent:        protocol.DefaultAckDelayExponent,
 			StatelessResetToken:     &protocol.StatelessResetToken{},
-			ActiveConnectionIDLimit: 2,
+			ActiveConnectionIDLimit: protocol.DefaultActiveConnectionIDLimit,
 		}).Marshal(protocol.PerspectiveServer)
 		p := &TransportParameters{}
 		Expect(p.Unmarshal(data, protocol.PerspectiveServer)).To(Succeed())
 		Expect(p.AckDelayExponent).To(BeEquivalentTo(protocol.DefaultAckDelayExponent))
+		Expect(p.ActiveConnectionIDLimit).To(BeEquivalentTo(protocol.DefaultActiveConnectionIDLimit))
 	})
 
 	It("errors when the varint value has the wrong length", func() {
diff --git a/packet_packer_test.go b/packet_packer_test.go
index 31caa17a..05dc8689 100644
--- a/packet_packer_test.go
+++ b/packet_packer_test.go
@@ -316,6 +316,12 @@ var _ = Describe("Packet packer", func() {
 				Expect(p.longHdrPackets[0].EncryptionLevel()).To(Equal(protocol.Encryption0RTT))
 				Expect(p.longHdrPackets[0].frames).To(Equal([]*ackhandler.Frame{cf}))
 			})
+
+			It("doesn't add an ACK-only 0-RTT packet", func() { // ACK frames cannot be sent in 0-RTT packets
+				p, err := packer.PackCoalescedPacket(true, protocol.Version1)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(p).To(BeNil())
+			})
 		})
 
 		Context("packing CONNECTION_CLOSE", func() {
diff --git a/sys_conn_helper_linux_test.go b/sys_conn_helper_linux_test.go
new file mode 100644
index 00000000..fa39c523
--- /dev/null
+++ b/sys_conn_helper_linux_test.go
@@ -0,0 +1,70 @@
+// We need root permissions to use RCVBUFFORCE.
+// This test is therefore only compiled when the root build flag is set.
+// It can only succeed if the tests are then also run with root permissions.
+//go:build linux && root
+
+package quic
+
+import (
+	"net"
+	"os"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("forcing a change of send and receive buffer sizes", func() {
+	It("forces a change of the receive buffer size", func() {
+		if os.Getuid() != 0 {
+			Fail("Must be root to force change the receive buffer size")
+		}
+
+		c, err := net.ListenPacket("udp", "127.0.0.1:0")
+		Expect(err).ToNot(HaveOccurred())
+		defer c.Close()
+		syscallConn, err := c.(*net.UDPConn).SyscallConn()
+		Expect(err).ToNot(HaveOccurred())
+
+		const small = 256 << 10 // 256 KB
+		Expect(forceSetReceiveBuffer(syscallConn, small)).To(Succeed())
+
+		size, err := inspectReadBuffer(syscallConn)
+		Expect(err).ToNot(HaveOccurred())
+		//  The kernel doubles this value (to allow space for bookkeeping overhead)
+		Expect(size).To(Equal(2 * small))
+
+		const large = 32 << 20 // 32 MB
+		Expect(forceSetReceiveBuffer(syscallConn, large)).To(Succeed())
+		size, err = inspectReadBuffer(syscallConn)
+		Expect(err).ToNot(HaveOccurred())
+		//  The kernel doubles this value (to allow space for bookkeeping overhead)
+		Expect(size).To(Equal(2 * large))
+	})
+
+	It("forces a change of the send buffer size", func() {
+		if os.Getuid() != 0 {
+			Fail("Must be root to force change the send buffer size")
+		}
+
+		c, err := net.ListenPacket("udp", "127.0.0.1:0")
+		Expect(err).ToNot(HaveOccurred())
+		defer c.Close()
+		syscallConn, err := c.(*net.UDPConn).SyscallConn()
+		Expect(err).ToNot(HaveOccurred())
+
+		const small = 256 << 10 // 256 KB
+		Expect(forceSetSendBuffer(syscallConn, small)).To(Succeed())
+
+		size, err := inspectWriteBuffer(syscallConn)
+		Expect(err).ToNot(HaveOccurred())
+		//  The kernel doubles this value (to allow space for bookkeeping overhead)
+		Expect(size).To(Equal(2 * small))
+
+		const large = 32 << 20 // 32 MB
+		Expect(forceSetSendBuffer(syscallConn, large)).To(Succeed())
+		size, err = inspectWriteBuffer(syscallConn)
+		Expect(err).ToNot(HaveOccurred())
+		//  The kernel doubles this value (to allow space for bookkeeping overhead)
+		Expect(size).To(Equal(2 * large))
+	})
+})
diff --git a/transport_test.go b/transport_test.go
index 9441e4de..f60db3b2 100644
--- a/transport_test.go
+++ b/transport_test.go
@@ -235,13 +235,16 @@ var _ = Describe("Transport", func() {
 		Expect(err).ToNot(HaveOccurred())
 		b = append(b, token[:]...)
 		conn := NewMockPacketHandler(mockCtrl)
+		destroyed := make(chan struct{})
 		gomock.InOrder(
 			phm.EXPECT().GetByResetToken(token).Return(conn, true),
 			conn.EXPECT().destroy(gomock.Any()).Do(func(err error) {
 				Expect(err).To(MatchError(&StatelessResetError{Token: token}))
+				close(destroyed)
 			}),
 		)
 		packetChan <- packetToRead{data: b}
+		Eventually(destroyed).Should(BeClosed())
 
 		// shutdown
 		phm.EXPECT().Close(gomock.Any())