Repository: quic-go/quic-go

Issue:
unsuccessful Encrypted Client Hello (ech)
TLS ECH support was added in go 1.24. I can use it with the net/http client and server implementation to verify that it works. In my testing with it I do get a 200 OK response and the `http.request` does have the `TLS.ECHAccepted` flag set to true.
I tried to also use it with a quic-go/http3 client and server by only doing minimal changes to my setup. Basically, I switched out:
http.server -> http3.server 
http.Transport -> http3.Transport
but with these changes the TLS handshake is now no longer successful. 
On the http3 server this message always pops up when a client tries to connect with a populated `EncryptedClientHelloConfigList` in the `tls.Config`
`CRYPTO_ERROR 0x179 (remote): tls: encrypted client hello required`
And on the client I see the following response:
`CRYPTO_ERROR 0x179 (local): tls: server rejected ECH`

In Issue #3905 it was mentioned that ECH is 
> purely a crypto/tls issue

this makes me think that it should work with quic-go even if quic-go is still on 1.23, but it seems to me like somewhere along the line the ECH TLS extension is dropped.
Here is the setup I used [https://github.com/LeonardWalter/echTester](https://github.com/LeonardWalter/echTester)

Discussion:
@marten-seemann: Have you checked the TLS messages sent with Wireshark?@LeonardWalter: Yes, the ECH extension is transmitted:


With HTTP2 client hello
```
Frame 4: 1648 bytes on wire (13184 bits), 1648 bytes captured (13184 bits)
Ethernet II, Src: 00:00:00_00:00:00 (00:00:00:00:00:00), Dst: 00:00:00_00:00:00 (00:00:00:00:00:00)
Internet Protocol Version 4, Src: 127.0.0.1, Dst: 127.0.0.1
Transmission Control Protocol, Src Port: 44164, Dst Port: 17443, Seq: 1, Ack: 1, Len: 1582
Transport Layer Security
    TLSv1.3 Record Layer: Handshake Protocol: Client Hello
        Content Type: Handshake (22)
        Version: TLS 1.0 (0x0301)
        Length: 1577
        Handshake Protocol: Client Hello
            Handshake Type: Client Hello (1)
            Length: 1573
            Version: TLS 1.2 (0x0303)
            Random: 5f489ce1d0cbbb8909786b68e4cac177bb3ff7e41734420a4c1a8e67915759fc
            Session ID Length: 32
            Session ID: 9d3554eeddde9d3d4dad090da60f68b714e9d8a03734620318cc47d546204b7e
            Cipher Suites Length: 6
            Cipher Suites (3 suites)
            Compression Methods Length: 1
            Compression Methods (1 method)
            Extensions Length: 1494
            Extension: server_name (len=13) name=xxxx
            Extension: signed_certificate_timestamp (len=0)
            Extension: encrypted_client_hello (len=145)
                Type: encrypted_client_hello (65037)
                Length: 145
                Client Hello type: Outer Client Hello (0)
                Cipher Suite: HKDF-SHA256/AES-128-GCM
                Config Id: 31
                Enc length: 32
                Enc: 185a49bb82906997dfd8d35e9a97d1e95654d37f61e8e8ef6014d09d98edee49
                Payload length: 103
                Payload: 7e388609ce76147bd9b46a2b580a3829cce3c07a0de94f9f38947719c7e1073f7e90bb665dacd774c905324f6b75f258eddfe32281e3a4cee3587ef97e793bf3c2e3e62cc5782ebe8b7e8d285f581cfddf379f7a6781329a100ed909ccf2ee5f2b32870611c3d0
            Extension: status_request (len=5)
            Extension: supported_groups (len=12)
            Extension: signature_algorithms (len=26)
            Extension: supported_versions (len=3) TLS 1.3
            Extension: key_share (len=1258) Unknown (4588), x25519
```

Capture from HTTP3 client hello

```
Frame 2: 1322 bytes on wire (10576 bits), 1322 bytes captured (10576 bits)
Ethernet II, Src: 00:00:00_00:00:00 (00:00:00:00:00:00), Dst: 00:00:00_00:00:00 (00:00:00:00:00:00)
Internet Protocol Version 4, Src: 127.0.0.1, Dst: 127.0.0.1
User Datagram Protocol, Src Port: 34903, Dst Port: 17443
QUIC IETF
    QUIC Connection information
    [Packet Length: 1280]
    1... .... = Header Form: Long Header (1)
    .1.. .... = Fixed Bit: True
    ..00 .... = Packet Type: Initial (0)
    [.... 00.. = Reserved: 0]
    [.... ..01 = Packet Number Length: 2 bytes (1)]
    Version: 1 (0x00000001)
    Destination Connection ID Length: 15
    Destination Connection ID: 1371cd7564b1cdcefb0cb4624c56c4
    Source Connection ID Length: 4
    Source Connection ID: 0ebb0605
    Token Length: 0
    Length: 1251
    [Packet Number: 1]
    Payload [truncated]: f22538de22e2d7879a5087df13225b56154562d96fe9dc2e3e2aad1cac62d3364cf3165bee49af28ae352985f945c84ffc547a6c7fd82822b42163de20e415582777f6453a57d2a1cfb0dc614de3901c4e8d7b9cb17dfea50b767292b548f310a139605694c6e69ed2da0b9dc4
    PADDING Length: 764
    CRYPTO
        Frame Type: CRYPTO (0x0000000000000006)
        Offset: 1229
        Length: 464
        Crypto Data
        TLSv1.3 Record Layer: Handshake Protocol: Client Hello
            Handshake Protocol: Client Hello (last fragment)
            [2 Reassembled Handshake Fragments (1693 bytes): #1(1229), #2(464)]
            Handshake Protocol: Client Hello
                Handshake Type: Client Hello (1)
                Length: 1689
                Version: TLS 1.2 (0x0303)
                Random: 8f0c329dece8fa3d3cf641d8b873d97caa3d62d973992043feef998cc06b7503
                Session ID Length: 0
                Cipher Suites Length: 6
                Cipher Suites (3 suites)
                Compression Methods Length: 1
                Compression Methods (1 method)
                Extensions Length: 1642
                Extension: server_name (len=13) name=xxxx
                Extension: signed_certificate_timestamp (len=0)
                Extension: quic_transport_parameters (len=71)
                Extension: encrypted_client_hello (len=209)
                    Type: encrypted_client_hello (65037)
                    Length: 209
                    Client Hello type: Outer Client Hello (0)
                    Cipher Suite: HKDF-SHA256/AES-128-GCM
                    Config Id: 31
                    Enc length: 32
                    Enc: af61aca54e49bbd1dd89494c7a0bb29ae65e2fed9e30a06e45df6a6199d58c5a
                    Payload length: 167
                    Payload [truncated]: cd0c7481aaacc1a919ff035e0eef236de1ac7c0d8e54bccdf7d3834cc5f559da83506a46f76d442c0a7d3a10154789759272d546390b1115966d1489b55db4725b85f81f5e281945eea6f80e5e3dc3e6712931eba054e82bdb0b9adeec1e1b7594f13ee9881ec559d5cdcc5cad
                Extension: status_request (len=5)
                Extension: supported_groups (len=12)
                Extension: signature_algorithms (len=26)
                Extension: application_layer_protocol_negotiation (len=5)
                Extension: supported_versions (len=3) TLS 1.3
                Extension: key_share (len=1258) Unknown (4588), x25519
```

So probably there is some problem during parsing on server side.@marten-seemann: I'm marking this as "help wanted". This will require some digging into what the standard library sees and expects, but this has really low priority for me at this point. Contributions welcome! Or alternatively, contact me if you want to see resolved soon.

Solution patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 6006b36a..b8b00f0b 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -41,7 +41,7 @@ jobs:
       matrix:
         go: [  "1.23.x", "1.24.x" ]
     env:
-      GOLANGCI_LINT_VERSION: v1.64.4
+      GOLANGCI_LINT_VERSION: v2.0.2
     name: golangci-lint (Go ${{ matrix.go }})
     steps:
       - uses: actions/checkout@v4
@@ -49,13 +49,13 @@ jobs:
         with:
           go-version: ${{ matrix.go }}
       - name: golangci-lint (Linux)
-        uses: golangci/golangci-lint-action@v6
+        uses: golangci/golangci-lint-action@v7
         with:
           args: --timeout=3m
           version: ${{ env.GOLANGCI_LINT_VERSION }}
       - name: golangci-lint (Windows)
         if: success() || failure() # run this step even if the previous one failed
-        uses: golangci/golangci-lint-action@v6
+        uses: golangci/golangci-lint-action@v7
         env:
           GOOS: "windows"
         with:
@@ -63,7 +63,7 @@ jobs:
           version: ${{ env.GOLANGCI_LINT_VERSION }}
       - name: golangci-lint (OSX)
         if: success() || failure() # run this step even if the previous one failed
-        uses: golangci/golangci-lint-action@v6
+        uses: golangci/golangci-lint-action@v7
         env:
           GOOS: "darwin"
         with:
@@ -71,7 +71,7 @@ jobs:
           version: ${{ env.GOLANGCI_LINT_VERSION }}
       - name: golangci-lint (FreeBSD)
         if: success() || failure() # run this step even if the previous one failed
-        uses: golangci/golangci-lint-action@v6
+        uses: golangci/golangci-lint-action@v7
         env:
           GOOS: "freebsd"
         with:
@@ -79,7 +79,7 @@ jobs:
           version: ${{ env.GOLANGCI_LINT_VERSION }}
       - name: golangci-lint (others)
         if: success() || failure() # run this step even if the previous one failed
-        uses: golangci/golangci-lint-action@v6
+        uses: golangci/golangci-lint-action@v7
         env:
           GOOS: "solaris" # some OS that we don't have any build tags for
         with:
diff --git a/.golangci.yml b/.golangci.yml
index 1174b125..cf30c9d4 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,51 +1,66 @@
-linters-settings:
-  misspell:
-    ignore-words:
-      - ect
-  depguard:
-    rules:
-      quicvarint:
-        list-mode: strict
-        files:
-          - "**/github.com/quic-go/quic-go/quicvarint/*"
-          - "!$test"
-        allow:
-          - $gostd
-
+version: "2"
 linters:
-  disable-all: true
+  default: none
   enable:
     - asciicheck
     - copyloopvar
     - depguard
     - exhaustive
-    - goimports
-    - gofmt # redundant, since gofmt *should* be a no-op after gofumpt
-    - gofumpt
-    - gosimple
     - govet
     - ineffassign
     - misspell
     - prealloc
     - staticcheck
-    - stylecheck
     - unconvert
     - unparam
     - unused
-
-issues:
-  exclude-files:
-    - internal/handshake/cipher_suite.go
-  exclude-rules:
-    - path: internal/qtls
-      linters:
-        - depguard
-    - path: _test\.go
-      linters:
-        - exhaustive
-        - prealloc
-        - unparam
-    - path: _test\.go
-      text: "SA1029:"
-      linters:
-        - staticcheck
+  settings:
+    depguard:
+      rules:
+        quicvarint:
+          list-mode: strict
+          files:
+            - '**/github.com/quic-go/quic-go/quicvarint/*'
+            - '!$test'
+          allow:
+            - $gostd
+    misspell:
+      ignore-rules:
+        - ect
+  exclusions:
+    generated: lax
+    presets:
+      - comments
+      - common-false-positives
+      - legacy
+      - std-error-handling
+    rules:
+      - linters:
+          - depguard
+        path: internal/qtls
+      - linters:
+          - exhaustive
+          - prealloc
+          - unparam
+        path: _test\.go
+      - linters:
+          - staticcheck
+        path: _test\.go
+        text: 'SA1029:'
+    paths:
+      - internal/handshake/cipher_suite.go
+      - third_party$
+      - builtin$
+      - examples$
+formatters:
+  enable:
+    - gofmt
+    - gofumpt
+    - goimports
+  exclusions:
+    generated: lax
+    paths:
+      - internal/handshake/cipher_suite.go
+      - third_party$
+      - builtin$
+      - examples$
diff --git a/connection.go b/connection.go
index ef8476eb..4ed639ac 100644
--- a/connection.go
+++ b/connection.go
@@ -597,7 +597,7 @@ runLoop:
 		// We don't need to wait for new events if:
 		// * we processed packets: we probably need to send an ACK, and potentially more data
 		// * the pacer allows us to send more packets immediately
-		shouldProceedImmediately := sendQueueAvailable == nil && (processed || s.pacingDeadline == deadlineSendImmediately)
+		shouldProceedImmediately := sendQueueAvailable == nil && (processed || s.pacingDeadline.Equal(deadlineSendImmediately))
 		if !shouldProceedImmediately {
 			// 3rd: wait for something to happen:
 			// * closing of the connection
@@ -2141,10 +2141,11 @@ func (s *connection) sendPackets(now time.Time) error {
 		if err := s.sendPackedCoalescedPacket(packet, s.sentPacketHandler.ECNMode(packet.IsOnlyShortHeaderPacket()), now); err != nil {
 			return err
 		}
-		sendMode := s.sentPacketHandler.SendMode(now)
-		if sendMode == ackhandler.SendPacingLimited {
+		//nolint:exhaustive // only need to handle pacing-related events here
+		switch s.sentPacketHandler.SendMode(now) {
+		case ackhandler.SendPacingLimited:
 			s.resetPacingDeadline()
-		} else if sendMode == ackhandler.SendAny {
+		case ackhandler.SendAny:
 			s.pacingDeadline = deadlineSendImmediately
 		}
 		return nil
diff --git a/http3/client.go b/http3/client.go
index 9214bb80..91f43e41 100644
--- a/http3/client.go
+++ b/http3/client.go
@@ -109,24 +109,24 @@ func newClientConn(
 			if c.logger != nil {
 				c.logger.Debug("Setting up connection failed", "error", err)
 			}
-			c.connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeInternalError), "")
+			c.CloseWithError(quic.ApplicationErrorCode(ErrCodeInternalError), "")
 		}
 	}()
 	if streamHijacker != nil {
 		go c.handleBidirectionalStreams(streamHijacker)
 	}
-	go c.connection.handleUnidirectionalStreams(uniStreamHijacker)
+	go c.handleUnidirectionalStreams(uniStreamHijacker)
 	return c
 }
 
 // OpenRequestStream opens a new request stream on the HTTP/3 connection.
 func (c *ClientConn) OpenRequestStream(ctx context.Context) (RequestStream, error) {
-	return c.connection.openRequestStream(ctx, c.requestWriter, nil, c.disableCompression, c.maxResponseHeaderBytes)
+	return c.openRequestStream(ctx, c.requestWriter, nil, c.disableCompression, c.maxResponseHeaderBytes)
 }
 
 func (c *ClientConn) setupConn() error {
 	// open the control stream
-	str, err := c.connection.OpenUniStream()
+	str, err := c.OpenUniStream()
 	if err != nil {
 		return err
 	}
@@ -140,7 +140,7 @@ func (c *ClientConn) setupConn() error {
 
 func (c *ClientConn) handleBidirectionalStreams(streamHijacker func(FrameType, quic.ConnectionTracingID, quic.Stream, error) (hijacked bool, err error)) {
 	for {
-		str, err := c.connection.AcceptStream(context.Background())
+		str, err := c.AcceptStream(context.Background())
 		if err != nil {
 			if c.logger != nil {
 				c.logger.Debug("accepting bidirectional stream failed", "error", err)
@@ -151,7 +151,7 @@ func (c *ClientConn) handleBidirectionalStreams(streamHijacker func(FrameType, q
 			r:    str,
 			conn: &c.connection,
 			unknownFrameHandler: func(ft FrameType, e error) (processed bool, err error) {
-				id := c.connection.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)
+				id := c.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)
 				return streamHijacker(ft, id, str, e)
 			},
 		}
@@ -164,7 +164,7 @@ func (c *ClientConn) handleBidirectionalStreams(streamHijacker func(FrameType, q
 					c.logger.Debug("error handling stream", "error", err)
 				}
 			}
-			c.connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "received HTTP/3 frame on bidirectional stream")
+			c.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "received HTTP/3 frame on bidirectional stream")
 		}()
 	}
 }
@@ -210,17 +210,17 @@ func (c *ClientConn) roundTrip(req *http.Request) (*http.Response, error) {
 		connCtx := c.Connection.Context()
 		// wait for the server's SETTINGS frame to arrive
 		select {
-		case <-c.connection.ReceivedSettings():
+		case <-c.ReceivedSettings():
 		case <-connCtx.Done():
 			return nil, context.Cause(connCtx)
 		}
-		if !c.connection.Settings().EnableExtendedConnect {
+		if !c.Settings().EnableExtendedConnect {
 			return nil, errors.New("http3: server didn't enable Extended CONNECT")
 		}
 	}
 
 	reqDone := make(chan struct{})
-	str, err := c.connection.openRequestStream(
+	str, err := c.openRequestStream(
 		req.Context(),
 		c.requestWriter,
 		reqDone,
@@ -350,7 +350,7 @@ func (c *ClientConn) doRequest(req *http.Request, str *requestStream) (*http.Res
 		}
 		break
 	}
-	connState := c.connection.ConnectionState().TLS
+	connState := c.ConnectionState().TLS
 	res.TLS = &connState
 	res.Request = req
 	return res, nil
diff --git a/http3/conn.go b/http3/conn.go
index bb17a5e5..1a9a410e 100644
--- a/http3/conn.go
+++ b/http3/conn.go
@@ -106,7 +106,7 @@ func (c *connection) openRequestStream(
 	disableCompression bool,
 	maxHeaderBytes uint64,
 ) (*requestStream, error) {
-	str, err := c.Connection.OpenStreamSync(ctx)
+	str, err := c.OpenStreamSync(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -180,7 +180,7 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
 	)
 
 	for {
-		str, err := c.Connection.AcceptUniStream(context.Background())
+		str, err := c.AcceptUniStream(context.Background())
 		if err != nil {
 			if c.logger != nil {
 				c.logger.Debug("accepting unidirectional stream failed", "error", err)
@@ -191,7 +191,7 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
 		go func(str quic.ReceiveStream) {
 			streamType, err := quicvarint.Read(quicvarint.NewReader(str))
 			if err != nil {
-				id := c.Connection.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)
+				id := c.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)
 				if hijack != nil && hijack(StreamType(streamType), id, str, err) {
 					return
 				}
@@ -205,13 +205,13 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
 			case streamTypeControlStream:
 			case streamTypeQPACKEncoderStream:
 				if isFirst := rcvdQPACKEncoderStr.CompareAndSwap(false, true); !isFirst {
-					c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate QPACK encoder stream")
+					c.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate QPACK encoder stream")
 				}
 				// Our QPACK implementation doesn't use the dynamic table yet.
 				return
 			case streamTypeQPACKDecoderStream:
 				if isFirst := rcvdQPACKDecoderStr.CompareAndSwap(false, true); !isFirst {
-					c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate QPACK decoder stream")
+					c.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate QPACK decoder stream")
 				}
 				// Our QPACK implementation doesn't use the dynamic table yet.
 				return
@@ -219,17 +219,17 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
 				switch c.perspective {
 				case protocol.PerspectiveClient:
 					// we never increased the Push ID, so we don't expect any push streams
-					c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeIDError), "")
+					c.CloseWithError(quic.ApplicationErrorCode(ErrCodeIDError), "")
 				case protocol.PerspectiveServer:
 					// only the server can push
-					c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "")
+					c.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "")
 				}
 				return
 			default:
 				if hijack != nil {
 					if hijack(
 						StreamType(streamType),
-						c.Connection.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID),
+						c.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID),
 						str,
 						nil,
 					) {
@@ -267,8 +267,8 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
 			// If datagram support was enabled on our side as well as on the server side,
 			// we can expect it to have been negotiated both on the transport and on the HTTP/3 layer.
 			// Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT).
-			if c.enableDatagrams && !c.Connection.ConnectionState().SupportsDatagrams {
-				c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeSettingsError), "missing QUIC Datagram support")
+			if c.enableDatagrams && !c.ConnectionState().SupportsDatagrams {
+				c.CloseWithError(quic.ApplicationErrorCode(ErrCodeSettingsError), "missing QUIC Datagram support")
 				return
 			}
 			go func() {
@@ -287,22 +287,22 @@ func (c *connection) sendDatagram(streamID protocol.StreamID, b []byte) error {
 	data := make([]byte, 0, len(b)+8)
 	data = quicvarint.Append(data, uint64(streamID/4))
 	data = append(data, b...)
-	return c.Connection.SendDatagram(data)
+	return c.SendDatagram(data)
 }
 
 func (c *connection) receiveDatagrams() error {
 	for {
-		b, err := c.Connection.ReceiveDatagram(context.Background())
+		b, err := c.ReceiveDatagram(context.Background())
 		if err != nil {
 			return err
 		}
 		quarterStreamID, n, err := quicvarint.Parse(b)
 		if err != nil {
-			c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeDatagramError), "")
+			c.CloseWithError(quic.ApplicationErrorCode(ErrCodeDatagramError), "")
 			return fmt.Errorf("could not read quarter stream id: %w", err)
 		}
 		if quarterStreamID > maxQuarterStreamID {
-			c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeDatagramError), "")
+			c.CloseWithError(quic.ApplicationErrorCode(ErrCodeDatagramError), "")
 			return fmt.Errorf("invalid quarter stream id: %w", err)
 		}
 		streamID := protocol.StreamID(4 * quarterStreamID)
diff --git a/http3/http_stream.go b/http3/http_stream.go
index 4593d473..e030ab97 100644
--- a/http3/http_stream.go
+++ b/http3/http_stream.go
@@ -211,8 +211,8 @@ func (s *requestStream) ReadResponse() (*http.Response, error) {
 	}
 	frame, err := fp.ParseNext()
 	if err != nil {
-		s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeFrameError))
-		s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeFrameError))
+		s.CancelRead(quic.StreamErrorCode(ErrCodeFrameError))
+		s.CancelWrite(quic.StreamErrorCode(ErrCodeFrameError))
 		return nil, fmt.Errorf("http3: parsing frame failed: %w", err)
 	}
 	hf, ok := frame.(*headersFrame)
@@ -221,14 +221,14 @@ func (s *requestStream) ReadResponse() (*http.Response, error) {
 		return nil, errors.New("http3: expected first frame to be a HEADERS frame")
 	}
 	if hf.Length > s.maxHeaderBytes {
-		s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeFrameError))
-		s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeFrameError))
+		s.CancelRead(quic.StreamErrorCode(ErrCodeFrameError))
+		s.CancelWrite(quic.StreamErrorCode(ErrCodeFrameError))
 		return nil, fmt.Errorf("http3: HEADERS frame too large: %d bytes (max: %d)", hf.Length, s.maxHeaderBytes)
 	}
 	headerBlock := make([]byte, hf.Length)
 	if _, err := io.ReadFull(s.Stream, headerBlock); err != nil {
-		s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeRequestIncomplete))
-		s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeRequestIncomplete))
+		s.CancelRead(quic.StreamErrorCode(ErrCodeRequestIncomplete))
+		s.CancelWrite(quic.StreamErrorCode(ErrCodeRequestIncomplete))
 		return nil, fmt.Errorf("http3: failed to read response headers: %w", err)
 	}
 	hfs, err := s.decoder.DecodeFull(headerBlock)
@@ -239,8 +239,8 @@ func (s *requestStream) ReadResponse() (*http.Response, error) {
 	}
 	res := s.response
 	if err := updateResponseFromHeaders(res, hfs); err != nil {
-		s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeMessageError))
-		s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeMessageError))
+		s.CancelRead(quic.StreamErrorCode(ErrCodeMessageError))
+		s.CancelWrite(quic.StreamErrorCode(ErrCodeMessageError))
 		return nil, fmt.Errorf("http3: invalid response: %w", err)
 	}
 
diff --git a/http3/server.go b/http3/server.go
index 1479609c..e3ad5592 100644
--- a/http3/server.go
+++ b/http3/server.go
@@ -57,55 +57,24 @@ type QUICEarlyListener interface {
 
 var _ QUICEarlyListener = &quic.EarlyListener{}
 
-func versionToALPN(v protocol.Version) string {
-	//nolint:exhaustive // These are all the versions we care about.
-	switch v {
-	case protocol.Version1, protocol.Version2:
-		return NextProtoH3
-	default:
-		return ""
-	}
-}
-
 // ConfigureTLSConfig creates a new tls.Config which can be used
-// to create a quic.Listener meant for serving http3. The created
-// tls.Config adds the functionality of detecting the used QUIC version
-// in order to set the correct ALPN value for the http3 connection.
+// to create a quic.Listener meant for serving HTTP/3.
 func ConfigureTLSConfig(tlsConf *tls.Config) *tls.Config {
-	// The tls.Config used to setup the quic.Listener needs to have the GetConfigForClient callback set.
-	// That way, we can get the QUIC version and set the correct ALPN value.
-	return &tls.Config{
-		GetConfigForClient: func(ch *tls.ClientHelloInfo) (*tls.Config, error) {
-			// determine the ALPN from the QUIC version used
-			proto := NextProtoH3
-			val := ch.Context().Value(quic.QUICVersionContextKey)
-			if v, ok := val.(quic.Version); ok {
-				proto = versionToALPN(v)
-			}
-			config := tlsConf
-			if tlsConf.GetConfigForClient != nil {
-				getConfigForClient := tlsConf.GetConfigForClient
-				var err error
-				conf, err := getConfigForClient(ch)
-				if err != nil {
-					return nil, err
-				}
-				if conf != nil {
-					config = conf
-				}
-			}
-			if config == nil {
-				return nil, nil
+	// Workaround for https://github.com/golang/go/issues/60506.
+	// This initializes the session tickets _before_ cloning the config.
+	_, _ = tlsConf.DecryptTicket(nil, tls.ConnectionState{})
+	config := tlsConf.Clone()
+	config.NextProtos = []string{NextProtoH3}
+	if gfc := config.GetConfigForClient; gfc != nil {
+		config.GetConfigForClient = func(ch *tls.ClientHelloInfo) (*tls.Config, error) {
+			conf, err := gfc(ch)
+			if conf == nil || err != nil {
+				return conf, err
 			}
-			// Workaround for https://github.com/golang/go/issues/60506.
-			// This initializes the session tickets _before_ cloning the config.
-			_, _ = config.DecryptTicket(nil, tls.ConnectionState{})
-
-			config = config.Clone()
-			config.NextProtos = []string{proto}
-			return config, nil
-		},
+			return ConfigureTLSConfig(conf), nil
+		}
 	}
+	return config
 }
 
 // contextKey is a value for use with context.WithValue. It's used as
@@ -407,28 +376,10 @@ func (s *Server) generateAltSvcHeader() {
 	}
 
 	// This code assumes that we will use protocol.SupportedVersions if no quic.Config is passed.
-	supportedVersions := protocol.SupportedVersions
-	if s.QUICConfig != nil && len(s.QUICConfig.Versions) > 0 {
-		supportedVersions = s.QUICConfig.Versions
-	}
-
-	// keep track of which have been seen so we don't yield duplicate values
-	seen := make(map[string]struct{}, len(supportedVersions))
-	var versionStrings []string
-	for _, version := range supportedVersions {
-		if v := versionToALPN(version); len(v) > 0 {
-			if _, ok := seen[v]; !ok {
-				versionStrings = append(versionStrings, v)
-				seen[v] = struct{}{}
-			}
-		}
-	}
 
 	var altSvc []string
 	addPort := func(port int) {
-		for _, v := range versionStrings {
-			altSvc = append(altSvc, fmt.Sprintf(`%s=":%d"; ma=2592000`, v, port))
-		}
+		altSvc = append(altSvc, fmt.Sprintf(`%s=":%d"; ma=2592000`, NextProtoH3, port))
 	}
 
 	if s.Port != 0 {
diff --git a/http3/state_tracking_stream.go b/http3/state_tracking_stream.go
index 9cf17f5e..6edbeedf 100644
--- a/http3/state_tracking_stream.go
+++ b/http3/state_tracking_stream.go
@@ -90,7 +90,7 @@ func (s *stateTrackingStream) Close() error {
 }
 
 func (s *stateTrackingStream) CancelWrite(e quic.StreamErrorCode) {
-	s.closeSend(&quic.StreamError{StreamID: s.Stream.StreamID(), ErrorCode: e})
+	s.closeSend(&quic.StreamError{StreamID: s.StreamID(), ErrorCode: e})
 	s.Stream.CancelWrite(e)
 }
 
@@ -103,7 +103,7 @@ func (s *stateTrackingStream) Write(b []byte) (int, error) {
 }
 
 func (s *stateTrackingStream) CancelRead(e quic.StreamErrorCode) {
-	s.closeReceive(&quic.StreamError{StreamID: s.Stream.StreamID(), ErrorCode: e})
+	s.closeReceive(&quic.StreamError{StreamID: s.StreamID(), ErrorCode: e})
 	s.Stream.CancelRead(e)
 }
 
diff --git a/http3/transport.go b/http3/transport.go
index b90f1190..bdedb524 100644
--- a/http3/transport.go
+++ b/http3/transport.go
@@ -322,7 +322,7 @@ func (t *Transport) dial(ctx context.Context, hostname string) (quic.EarlyConnec
 		tlsConf.ServerName = sni
 	}
 	// Replace existing ALPNs by H3
-	tlsConf.NextProtos = []string{versionToALPN(t.QUICConfig.Versions[0])}
+	tlsConf.NextProtos = []string{NextProtoH3}
 
 	dial := t.Dial
 	if dial == nil {
diff --git a/internal/flowcontrol/connection_flow_controller.go b/internal/flowcontrol/connection_flow_controller.go
index bbeb7889..c550e75a 100644
--- a/internal/flowcontrol/connection_flow_controller.go
+++ b/internal/flowcontrol/connection_flow_controller.go
@@ -61,8 +61,8 @@ func (c *connectionFlowController) AddBytesRead(n protocol.ByteCount) (hasWindow
 	c.mutex.Lock()
 	defer c.mutex.Unlock()
 
-	c.baseFlowController.addBytesRead(n)
-	return c.baseFlowController.hasWindowUpdate()
+	c.addBytesRead(n)
+	return c.hasWindowUpdate()
 }
 
 func (c *connectionFlowController) GetWindowUpdate(now time.Time) protocol.ByteCount {
@@ -70,7 +70,7 @@ func (c *connectionFlowController) GetWindowUpdate(now time.Time) protocol.ByteC
 	defer c.mutex.Unlock()
 
 	oldWindowSize := c.receiveWindowSize
-	offset := c.baseFlowController.getWindowUpdate(now)
+	offset := c.getWindowUpdate(now)
 	if c.logger.Debug() && oldWindowSize < c.receiveWindowSize {
 		c.logger.Debugf("Increasing receive flow control window for the connection to %d kB", c.receiveWindowSize/(1<<10))
 	}
diff --git a/internal/flowcontrol/stream_flow_controller.go b/internal/flowcontrol/stream_flow_controller.go
index ba005122..968e9a5b 100644
--- a/internal/flowcontrol/stream_flow_controller.go
+++ b/internal/flowcontrol/stream_flow_controller.go
@@ -100,7 +100,7 @@ func (c *streamFlowController) UpdateHighestReceived(offset protocol.ByteCount,
 
 func (c *streamFlowController) AddBytesRead(n protocol.ByteCount) (hasStreamWindowUpdate, hasConnWindowUpdate bool) {
 	c.mutex.Lock()
-	c.baseFlowController.addBytesRead(n)
+	c.addBytesRead(n)
 	hasStreamWindowUpdate = c.shouldQueueWindowUpdate()
 	c.mutex.Unlock()
 	hasConnWindowUpdate = c.connection.AddBytesRead(n)
@@ -145,7 +145,7 @@ func (c *streamFlowController) GetWindowUpdate(now time.Time) protocol.ByteCount
 	defer c.mutex.Unlock()
 
 	oldWindowSize := c.receiveWindowSize
-	offset := c.baseFlowController.getWindowUpdate(now)
+	offset := c.getWindowUpdate(now)
 	if c.receiveWindowSize > oldWindowSize { // auto-tuning enlarged the window size
 		c.logger.Debugf("Increasing receive flow control window for stream %d to %d", c.streamID, c.receiveWindowSize)
 		c.connection.EnsureMinimumWindowSize(protocol.ByteCount(float64(c.receiveWindowSize)*protocol.ConnectionFlowControlMultiplier), now)
diff --git a/internal/utils/buffered_write_closer.go b/internal/utils/buffered_write_closer.go
index b5b9d6fc..80ebfecb 100644
--- a/internal/utils/buffered_write_closer.go
+++ b/internal/utils/buffered_write_closer.go
@@ -19,7 +19,7 @@ func NewBufferedWriteCloser(writer *bufio.Writer, closer io.Closer) io.WriteClos
 }
 
 func (h bufferedWriteCloser) Close() error {
-	if err := h.Writer.Flush(); err != nil {
+	if err := h.Flush(); err != nil {
 		return err
 	}
 	return h.Closer.Close()
diff --git a/internal/wire/new_connection_id_frame.go b/internal/wire/new_connection_id_frame.go
index 852d46ef..6f2287f4 100644
--- a/internal/wire/new_connection_id_frame.go
+++ b/internal/wire/new_connection_id_frame.go
@@ -30,7 +30,7 @@ func parseNewConnectionIDFrame(b []byte, _ protocol.Version) (*NewConnectionIDFr
 	}
 	b = b[l:]
 	if ret > seq {
-		//nolint:stylecheck
+		//nolint:staticcheck // SA1021: Retire Prior To is the name of the field
 		return nil, 0, fmt.Errorf("Retire Prior To value (%d) larger than Sequence Number (%d)", ret, seq)
 	}
 	if len(b) == 0 {
diff --git a/internal/wire/version_negotiation.go b/internal/wire/version_negotiation.go
index a551aa8c..407cb629 100644
--- a/internal/wire/version_negotiation.go
+++ b/internal/wire/version_negotiation.go
@@ -16,11 +16,11 @@ func ParseVersionNegotiationPacket(b []byte) (dest, src protocol.ArbitraryLenCon
 	}
 	b = b[n:]
 	if len(b) == 0 {
-		//nolint:stylecheck
+		//nolint:staticcheck // SA1021: the packet is called Version Negotiation packet
 		return nil, nil, nil, errors.New("Version Negotiation packet has empty version list")
 	}
 	if len(b)%4 != 0 {
-		//nolint:stylecheck
+		//nolint:staticcheck // SA1021: the packet is called Version Negotiation packet
 		return nil, nil, nil, errors.New("Version Negotiation packet has a version list with an invalid length")
 	}
 	versions := make([]protocol.Version, len(b)/4)
diff --git a/metrics/tracer.go b/metrics/tracer.go
index 293291db..ffcdfd91 100644
--- a/metrics/tracer.go
+++ b/metrics/tracer.go
@@ -71,10 +71,11 @@ func NewTracerWithRegisterer(registerer prometheus.Registerer) *logging.Tracer {
 			defer putStringSlice(tags)
 
 			var reason string
-			switch {
-			case hdr.Type == protocol.PacketTypeRetry:
+			//nolint:exhaustive // we only care about Retry and Initial packets here
+			switch hdr.Type {
+			case protocol.PacketTypeRetry:
 				reason = "retry"
-			case hdr.Type == protocol.PacketTypeInitial:
+			case protocol.PacketTypeInitial:
 				var ccf *logging.ConnectionCloseFrame
 				for _, f := range frames {
 					cc, ok := f.(*logging.ConnectionCloseFrame)
diff --git a/packet_unpacker.go b/packet_unpacker.go
index 9e0fa9d9..2f607fbd 100644
--- a/packet_unpacker.go
+++ b/packet_unpacker.go
@@ -197,8 +197,7 @@ func (u *packetUnpacker) unpackLongHeader(hd headerDecryptor, hdr *wire.Header,
 func unpackLongHeader(hd headerDecryptor, hdr *wire.Header, data []byte) (*wire.ExtendedHeader, error) {
 	hdrLen := hdr.ParsedLen()
 	if protocol.ByteCount(len(data)) < hdrLen+4+16 {
-		//nolint:stylecheck
-		return nil, fmt.Errorf("Packet too small. Expected at least 20 bytes after the header, got %d", protocol.ByteCount(len(data))-hdrLen)
+		return nil, fmt.Errorf("packet too small, expected at least 20 bytes after the header, got %d", protocol.ByteCount(len(data))-hdrLen)
 	}
 	// The packet number can be up to 4 bytes long, but we won't know the length until we decrypt it.
 	// 1. save a copy of the 4 bytes
diff --git a/quicvarint/io.go b/quicvarint/io.go
index 9368d1c5..27def409 100644
--- a/quicvarint/io.go
+++ b/quicvarint/io.go
@@ -31,7 +31,7 @@ func NewReader(r io.Reader) Reader {
 
 func (r *byteReader) ReadByte() (byte, error) {
 	var b [1]byte
-	n, err := r.Reader.Read(b[:])
+	n, err := r.Read(b[:])
 	if n == 1 && err == io.EOF {
 		err = nil
 	}
@@ -63,6 +63,6 @@ func NewWriter(w io.Writer) Writer {
 }
 
 func (w *byteWriter) WriteByte(c byte) error {
-	_, err := w.Writer.Write([]byte{c})
+	_, err := w.Write([]byte{c})
 	return err
 }
diff --git a/quicvarint/varint.go b/quicvarint/varint.go
index 9a22e334..f095e298 100644
--- a/quicvarint/varint.go
+++ b/quicvarint/varint.go
@@ -125,17 +125,18 @@ func AppendWithLen(b []byte, i uint64, length int) []byte {
 	if l > length {
 		panic(fmt.Sprintf("cannot encode %d in %d bytes", i, length))
 	}
-	if length == 2 {
+	switch length {
+	case 2:
 		b = append(b, 0b01000000)
-	} else if length == 4 {
+	case 4:
 		b = append(b, 0b10000000)
-	} else if length == 8 {
+	case 8:
 		b = append(b, 0b11000000)
 	}
-	for j := 1; j < length-l; j++ {
+	for range length - l - 1 {
 		b = append(b, 0)
 	}
-	for j := 0; j < l; j++ {
+	for j := range l {
 		b = append(b, uint8(i>>(8*(l-1-j))))
 	}
 	return b
diff --git a/sys_conn.go b/sys_conn.go
index 811131d9..6bfa3011 100644
--- a/sys_conn.go
+++ b/sys_conn.go
@@ -92,7 +92,7 @@ func (c *basicConn) ReadPacket() (receivedPacket, error) {
 	// The packet size should not exceed protocol.MaxPacketBufferSize bytes
 	// If it does, we only read a truncated packet, which will then end up undecryptable
 	buffer.Data = buffer.Data[:protocol.MaxPacketBufferSize]
-	n, addr, err := c.PacketConn.ReadFrom(buffer.Data)
+	n, addr, err := c.ReadFrom(buffer.Data)
 	if err != nil {
 		return receivedPacket{}, err
 	}
@@ -111,7 +111,7 @@ func (c *basicConn) WritePacket(b []byte, addr net.Addr, _ []byte, gsoSize uint1
 	if ecn != protocol.ECNUnsupported {
 		panic("cannot use ECN with a basicConn")
 	}
-	return c.PacketConn.WriteTo(b, addr)
+	return c.WriteTo(b, addr)
 }
 
 func (c *basicConn) capabilities() connCapabilities { return connCapabilities{DF: c.supportsDF} }
diff --git a/sys_conn_oob.go b/sys_conn_oob.go
index 75979682..5ed1b656 100644
--- a/sys_conn_oob.go
+++ b/sys_conn_oob.go
@@ -257,7 +257,7 @@ func (c *oobConn) WritePacket(b []byte, addr net.Addr, packetInfoOOB []byte, gso
 			}
 		}
 	}
-	n, _, err := c.OOBCapablePacketConn.WriteMsgUDP(b, oob, addr.(*net.UDPAddr))
+	n, _, err := c.WriteMsgUDP(b, oob, addr.(*net.UDPAddr))
 	return n, err
 }
 



Test patch:
diff --git a/integrationtests/self/close_test.go b/integrationtests/self/close_test.go
index 457f7bd9..e8e82487 100644
--- a/integrationtests/self/close_test.go
+++ b/integrationtests/self/close_test.go
@@ -131,7 +131,7 @@ func newBrokenConn(conn net.PacketConn) *brokenConn {
 	go func() {
 		<-c.broken
 		// make calls to ReadFrom return
-		c.PacketConn.SetDeadline(time.Now())
+		c.SetDeadline(time.Now())
 	}()
 	return c
 }
diff --git a/integrationtests/self/http_hotswap_test.go b/integrationtests/self/http_hotswap_test.go
index 65acfba8..638d8e24 100644
--- a/integrationtests/self/http_hotswap_test.go
+++ b/integrationtests/self/http_hotswap_test.go
@@ -50,7 +50,7 @@ func (ln *fakeClosingListener) Accept(ctx context.Context) (quic.EarlyConnection
 func (ln *fakeClosingListener) Close() error {
 	if ln.closed.CompareAndSwap(false, true) {
 		ln.cancel()
-		if ln.listenerWrapper.count.Add(-1) == 0 {
+		if ln.count.Add(-1) == 0 {
 			ln.listenerWrapper.Close()
 		}
 	}
diff --git a/integrationtests/self/multiplex_test.go b/integrationtests/self/multiplex_test.go
index 1a78c138..9437d311 100644
--- a/integrationtests/self/multiplex_test.go
+++ b/integrationtests/self/multiplex_test.go
@@ -326,10 +326,7 @@ func TestMultiplexingNonQUICPackets(t *testing.T) {
 	minExpected := numPackets * 4 / 5
 	timeout := time.After(time.Second)
 	var counter int
-	for {
-		if counter >= minExpected {
-			break
-		}
+	for counter < minExpected {
 		select {
 		case p := <-rcvdPackets:
 			require.Equal(t, tr1.Conn.LocalAddr(), p.addr, "non-QUIC packet received from wrong address")
diff --git a/integrationtests/self/zero_rtt_test.go b/integrationtests/self/zero_rtt_test.go
index 5453250f..87683d33 100644
--- a/integrationtests/self/zero_rtt_test.go
+++ b/integrationtests/self/zero_rtt_test.go
@@ -332,7 +332,7 @@ func Test0RTTWaitForHandshakeCompletion(t *testing.T) {
 	// check that 0-RTT packets only contain STREAM frames for the first stream
 	var num0RTT int
 	for _, p := range counter.getRcvdLongHeaderPackets() {
-		if p.hdr.Header.Type != protocol.PacketType0RTT {
+		if p.hdr.Type != protocol.PacketType0RTT {
 			continue
 		}
 		for _, f := range p.frames {
diff --git a/internal/qerr/errors_test.go b/internal/qerr/errors_test.go
index 44ab88e7..f217a48f 100644
--- a/internal/qerr/errors_test.go
+++ b/internal/qerr/errors_test.go
@@ -129,7 +129,7 @@ func TestHandshakeTimeoutError(t *testing.T) {
 	require.True(t, errors.Is(&HandshakeTimeoutError{}, &HandshakeTimeoutError{}))
 	require.False(t, errors.Is(&HandshakeTimeoutError{}, &IdleTimeoutError{}))
 
-	//nolint:gosimple // we need to assign to an interface here
+	//nolint:staticcheck // SA1021: we need to assign to an interface here
 	var err error
 	err = &HandshakeTimeoutError{}
 	nerr, ok := err.(net.Error)
@@ -143,7 +143,7 @@ func TestIdleTimeoutError(t *testing.T) {
 	require.True(t, errors.Is(&IdleTimeoutError{}, &IdleTimeoutError{}))
 	require.False(t, errors.Is(&IdleTimeoutError{}, &HandshakeTimeoutError{}))
 
-	//nolint:gosimple // we need to assign to an interface here
+	//nolint:staticcheck // SA1021: we need to assign to an interface here
 	var err error
 	err = &IdleTimeoutError{}
 	nerr, ok := err.(net.Error)
@@ -166,7 +166,7 @@ func TestStatelessResetError(t *testing.T) {
 	require.Equal(t, "received a stateless reset", (&StatelessResetError{}).Error())
 	require.True(t, errors.Is(&StatelessResetError{}, &StatelessResetError{}))
 
-	//nolint:gosimple // we need to assign to an interface here
+	//nolint:staticcheck // SA1021: we need to assign to an interface here
 	var err error
 	err = &StatelessResetError{}
 	nerr, ok := err.(net.Error)
diff --git a/packet_unpacker_test.go b/packet_unpacker_test.go
index f631c0d3..75f0db87 100644
--- a/packet_unpacker_test.go
+++ b/packet_unpacker_test.go
@@ -249,7 +249,7 @@ func TestUnpackHeaderSampleLongHeader(t *testing.T) {
 		cs.EXPECT().GetHandshakeOpener().Return(mocks.NewMockLongHeaderOpener(mockCtrl), nil)
 		_, err = unpacker.UnpackLongHeader(hdr, data[:len(data)-1])
 		require.IsType(t, &headerParseError{}, err)
-		require.ErrorContains(t, err, "Packet too small. Expected at least 20 bytes after the header, got 19")
+		require.ErrorContains(t, err, "packet too small, expected at least 20 bytes after the header, got 19")
 	})
 
 	t.Run("minimal size", func(t *testing.T) {
diff --git a/quicvarint/varint_test.go b/quicvarint/varint_test.go
index 5805a7b8..065119e5 100644
--- a/quicvarint/varint_test.go
+++ b/quicvarint/varint_test.go
@@ -253,7 +253,28 @@ func benchmarkAppend(b *testing.B, inputs []benchmarkValue) {
 		buf = Append(buf, inputs[index].v)
 
 		if !bytes.Equal(buf, inputs[index].b) {
-			b.Fatalf("expected to write %v, wrote %v", inputs[i].b, buf)
+			b.Fatalf("expected to write %v, wrote %v", inputs[index].b, buf)
+		}
+	}
+}
+
+func BenchmarkAppendWithLen(b *testing.B) {
+	b.Run("1-byte", func(b *testing.B) { benchmarkAppendWithLen(b, randomValues(min(b.N, 1024), maxVarInt1)) })
+	b.Run("2-byte", func(b *testing.B) { benchmarkAppendWithLen(b, randomValues(min(b.N, 1024), maxVarInt2)) })
+	b.Run("4-byte", func(b *testing.B) { benchmarkAppendWithLen(b, randomValues(min(b.N, 1024), maxVarInt4)) })
+	b.Run("8-byte", func(b *testing.B) { benchmarkAppendWithLen(b, randomValues(min(b.N, 1024), maxVarInt8)) })
+}
+
+func benchmarkAppendWithLen(b *testing.B, inputs []benchmarkValue) {
+	buf := make([]byte, 8)
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		buf = buf[:0]
+		index := i % 1024
+		buf = AppendWithLen(buf, inputs[index].v, len(inputs[index].b))
+
+		if !bytes.Equal(buf, inputs[index].b) {
+			b.Fatalf("expected to write %v, wrote %v", inputs[index].b, buf)
 		}
 	}
 }
diff --git a/send_stream_test.go b/send_stream_test.go
index 9e743e93..f28b2d78 100644
--- a/send_stream_test.go
+++ b/send_stream_test.go
@@ -1012,10 +1012,7 @@ func TestSendStreamRetransmitDataUntilAcknowledged(t *testing.T) {
 	mockSender.EXPECT().onStreamCompleted(streamID).Do(func(protocol.StreamID) { completed = true })
 
 	received := make([]byte, dataLen)
-	for {
-		if completed {
-			break
-		}
+	for !completed {
 		f, _, _ := str.popStreamFrame(protocol.ByteCount(mrand.Intn(300)+100), protocol.Version1)
 		if f.Frame == nil {
 			continue