Repository: quic-go/quic-go
Pull request #4181 (2023-11-23 08:40:18)

http3: reset stream when a handler panics
When debugging an [issue](https://github.com/caddyserver/caddy/issues/5951) for caddy, I found http3 server handler will not correctly propagate stream error. Code example [here](https://github.com/caddyserver/caddy/issues/5951#issuecomment-1823731441).

In this case `Content-Length` is set, http3 may use that info to infer the stream doesn't terminate properly. But in a streaming environment if there is an error upstream, client should be able to know there is an error at a stream level just like http2 regardless of the presence of `Content-Length`.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/quic-go/quic-go/pull/4181?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`771d136`)](https://app.codecov.io/gh/quic-go/quic-go/commit/771d136fa9d3e6ee835422319f2897840378b7b6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) 83.77% compared to head [(`0d45583`)](https://app.codecov.io/gh/quic-go/quic-go/pull/4181?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) 83.76%.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4181      +/-   ##
==========================================
- Coverage   83.77%   83.76%   -0.01%     
==========================================
  Files         150      150              
  Lines       15425    15430       +5     
==========================================
+ Hits        12922    12924       +2     
- Misses       2006     2008       +2     
- Partials      497      498       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/quic-go/quic-go/pull/4181?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).

WeidiDeng: I don't think status 500 is necessary in this case. Related [pr](https://github.com/quic-go/quic-go/pull/3950) and [issue](https://github.com/caddyserver/caddy/issues/5125).
marten-seemann: What does HTTP/2 do in both these cases?

Patch:
diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile
index b57da8ca..d27db151 100644
--- a/.clusterfuzzlite/Dockerfile
+++ b/.clusterfuzzlite/Dockerfile
@@ -3,7 +3,7 @@ FROM gcr.io/oss-fuzz-base/base-builder-go:v1
 ARG TARGETPLATFORM
 RUN echo "TARGETPLATFORM: ${TARGETPLATFORM}"
 
-ENV GOVERSION=1.20.7
+ENV GOVERSION=1.21.5
 
 RUN platform=$(echo ${TARGETPLATFORM} | tr '/' '-') && \
   filename="go${GOVERSION}.${platform}.tar.gz" && \
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index fd735c9b..5b6478e5 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -51,7 +51,7 @@ jobs:
         with:
           skip-pkg-cache: true
           args: --timeout=3m
-          version: v1.52.2
+          version: v1.55.2
       - name: golangci-lint (Windows)
         if: success() || failure() # run this step even if the previous one failed
         uses: golangci/golangci-lint-action@v3
@@ -60,7 +60,7 @@ jobs:
         with:
           skip-pkg-cache: true
           args: --timeout=3m
-          version: v1.52.2
+          version: v1.55.2
       - name: golangci-lint (OSX)
         if: success() || failure() # run this step even if the previous one failed
         uses: golangci/golangci-lint-action@v3
@@ -69,7 +69,7 @@ jobs:
         with:
           skip-pkg-cache: true
           args: --timeout=3m
-          version: v1.52.2
+          version: v1.55.2
       - name: golangci-lint (FreeBSD)
         if: success() || failure() # run this step even if the previous one failed
         uses: golangci/golangci-lint-action@v3
@@ -78,7 +78,7 @@ jobs:
         with:
           skip-pkg-cache: true
           args: --timeout=3m
-          version: v1.52.2
+          version: v1.55.2
       - name: golangci-lint (others)
         if: success() || failure() # run this step even if the previous one failed
         uses: golangci/golangci-lint-action@v3
@@ -87,4 +87,4 @@ jobs:
         with:
           skip-pkg-cache: true
           args: --timeout=3m
-          version: v1.52.2
+          version: v1.55.2
diff --git a/.golangci.yml b/.golangci.yml
index 1315759b..f5e9b48b 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -3,15 +3,15 @@ run:
     - internal/handshake/cipher_suite.go
 linters-settings:
   depguard:
-    type: blacklist
-    packages:
-      - github.com/marten-seemann/qtls
-      - github.com/quic-go/qtls-go1-19
-      - github.com/quic-go/qtls-go1-20
-    packages-with-error-message:
-      - github.com/marten-seemann/qtls: "importing qtls only allowed in internal/qtls"
-      - github.com/quic-go/qtls-go1-19: "importing qtls only allowed in internal/qtls"
-      - github.com/quic-go/qtls-go1-20: "importing qtls only allowed in internal/qtls"
+    rules:
+      qtls:
+        list-mode: lax
+        files:
+          - "!internal/qtls/**"
+          - "$all"
+        deny:
+          - pkg: github.com/quic-go/qtls-go1-20
+            desc: "importing qtls only allowed in internal/qtls"
   misspell:
     ignore-words:
       - ect
diff --git a/README.md b/README.md
index cfb4e612..cc4f609f 100644
--- a/README.md
+++ b/README.md
@@ -227,12 +227,13 @@ http.Client{
 ## Projects using quic-go
 
 | Project                                                   | Description                                                                                                                                                       | Stars                                                                                               |
-| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
+| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
 | [AdGuardHome](https://github.com/AdguardTeam/AdGuardHome) | Free and open source, powerful network-wide ads & trackers blocking DNS server.                                                                                   | ![GitHub Repo stars](https://img.shields.io/github/stars/AdguardTeam/AdGuardHome?style=flat-square) |
 | [algernon](https://github.com/xyproto/algernon)           | Small self-contained pure-Go web server with Lua, Markdown, HTTP/2, QUIC, Redis and PostgreSQL support                                                            | ![GitHub Repo stars](https://img.shields.io/github/stars/xyproto/algernon?style=flat-square)        |
 | [caddy](https://github.com/caddyserver/caddy/)            | Fast, multi-platform web server with automatic HTTPS                                                                                                              | ![GitHub Repo stars](https://img.shields.io/github/stars/caddyserver/caddy?style=flat-square)       |
 | [cloudflared](https://github.com/cloudflare/cloudflared)  | A tunneling daemon that proxies traffic from the Cloudflare network to your origins                                                                               | ![GitHub Repo stars](https://img.shields.io/github/stars/cloudflare/cloudflared?style=flat-square)  |
-| [go-libp2p](https://github.com/libp2p/go-libp2p)          | libp2p implementation in Go, powering [Kubo](https://github.com/ipfs/kubo) (IPFS) and [Lotus](https://github.com/filecoin-project/lotus) (Filecoin), among others | ![GitHub Repo stars](https://img.shields.io/github/stars/libp2p/go-libp2p?style=flat-square)        |
+| [go-libp2p](https://github.com/libp2p/go-libp2p)          | libp2p implementation in Go, powering [Kubo](https://github.com/ipfs/kubo) (IPFS) and [Lotus](https://github.com/filecoin-project/lotus) (Filecoin), among others | ![GitHub Repo stars](https://img.shields.io/github/stars/libp2p/go-libp2p?style=flat-square)     |
+| [gost](https://github.com/go-gost/gost)                   | A simple security tunnel written in Go                                                                                                                        | ![GitHub Repo stars](https://img.shields.io/github/stars/go-gost/gost?style=flat-square)            |
 | [Hysteria](https://github.com/apernet/hysteria)           | A powerful, lightning fast and censorship resistant proxy                                                                                                         | ![GitHub Repo stars](https://img.shields.io/github/stars/apernet/hysteria?style=flat-square)        |
 | [Mercure](https://github.com/dunglas/mercure)             | An open, easy, fast, reliable and battery-efficient solution for real-time communications                                                                         | ![GitHub Repo stars](https://img.shields.io/github/stars/dunglas/mercure?style=flat-square)         |
 | [OONI Probe](https://github.com/ooni/probe-cli)           | Next generation OONI Probe. Library and CLI tool.                                                                                                                 | ![GitHub Repo stars](https://img.shields.io/github/stars/ooni/probe-cli?style=flat-square)          |
diff --git a/connection.go b/connection.go
index 7f26ef5e..75ede6cb 100644
--- a/connection.go
+++ b/connection.go
@@ -307,7 +307,7 @@ var newConnection = func(
 		RetrySourceConnectionID:   retrySrcConnID,
 	}
 	if s.config.EnableDatagrams {
-		params.MaxDatagramFrameSize = protocol.MaxDatagramFrameSize
+		params.MaxDatagramFrameSize = wire.MaxDatagramSize
 	} else {
 		params.MaxDatagramFrameSize = protocol.InvalidByteCount
 	}
@@ -414,7 +414,7 @@ var newClientConnection = func(
 		InitialSourceConnectionID: srcConnID,
 	}
 	if s.config.EnableDatagrams {
-		params.MaxDatagramFrameSize = protocol.MaxDatagramFrameSize
+		params.MaxDatagramFrameSize = wire.MaxDatagramSize
 	} else {
 		params.MaxDatagramFrameSize = protocol.InvalidByteCount
 	}
@@ -629,7 +629,7 @@ runLoop:
 			sendQueueAvailable = s.sendQueue.Available()
 			continue
 		}
-		if err := s.triggerSending(); err != nil {
+		if err := s.triggerSending(now); err != nil {
 			s.closeLocal(err)
 		}
 		if s.sendQueue.WouldBlock() {
@@ -1522,7 +1522,7 @@ func (s *connection) handleAckFrame(frame *wire.AckFrame, encLevel protocol.Encr
 }
 
 func (s *connection) handleDatagramFrame(f *wire.DatagramFrame) error {
-	if f.Length(s.version) > protocol.MaxDatagramFrameSize {
+	if f.Length(s.version) > wire.MaxDatagramSize {
 		return &qerr.TransportError{
 			ErrorCode:    qerr.ProtocolViolation,
 			ErrorMessage: "DATAGRAM frame too large",
@@ -1767,9 +1767,8 @@ func (s *connection) applyTransportParameters() {
 	}
 }
 
-func (s *connection) triggerSending() error {
+func (s *connection) triggerSending(now time.Time) error {
 	s.pacingDeadline = time.Time{}
-	now := time.Now()
 
 	sendMode := s.sentPacketHandler.SendMode(now)
 	//nolint:exhaustive // No need to handle pacing limited here.
@@ -1801,7 +1800,7 @@ func (s *connection) triggerSending() error {
 			s.scheduleSending()
 			return nil
 		}
-		return s.triggerSending()
+		return s.triggerSending(now)
 	case ackhandler.SendPTOHandshake:
 		if err := s.sendProbePacket(protocol.EncryptionHandshake, now); err != nil {
 			return err
@@ -1810,7 +1809,7 @@ func (s *connection) triggerSending() error {
 			s.scheduleSending()
 			return nil
 		}
-		return s.triggerSending()
+		return s.triggerSending(now)
 	case ackhandler.SendPTOAppData:
 		if err := s.sendProbePacket(protocol.Encryption1RTT, now); err != nil {
 			return err
@@ -1819,7 +1818,7 @@ func (s *connection) triggerSending() error {
 			s.scheduleSending()
 			return nil
 		}
-		return s.triggerSending()
+		return s.triggerSending(now)
 	default:
 		return fmt.Errorf("BUG: invalid send mode %d", sendMode)
 	}
@@ -1988,7 +1987,7 @@ func (s *connection) maybeSendAckOnlyPacket(now time.Time) error {
 		if packet == nil {
 			return nil
 		}
-		return s.sendPackedCoalescedPacket(packet, ecn, time.Now())
+		return s.sendPackedCoalescedPacket(packet, ecn, now)
 	}
 
 	ecn := s.sentPacketHandler.ECNMode(true)
@@ -2350,7 +2349,9 @@ func (s *connection) SendDatagram(p []byte) error {
 
 	f := &wire.DatagramFrame{DataLenPresent: true}
 	if protocol.ByteCount(len(p)) > f.MaxDataLen(s.peerParams.MaxDatagramFrameSize, s.version) {
-		return errors.New("message too large")
+		return &DatagramTooLargeError{
+			PeerMaxDatagramFrameSize: int64(s.peerParams.MaxDatagramFrameSize),
+		}
 	}
 	f.Data = make([]byte, len(p))
 	copy(f.Data, p)
diff --git a/errors.go b/errors.go
index c9fb0a07..fda3c924 100644
--- a/errors.go
+++ b/errors.go
@@ -61,3 +61,15 @@ func (e *StreamError) Error() string {
 	}
 	return fmt.Sprintf("stream %d canceled by %s with error code %d", e.StreamID, pers, e.ErrorCode)
 }
+
+// DatagramTooLargeError is returned from Connection.SendDatagram if the payload is too large to be sent.
+type DatagramTooLargeError struct {
+	PeerMaxDatagramFrameSize int64
+}
+
+func (e *DatagramTooLargeError) Is(target error) bool {
+	_, ok := target.(*DatagramTooLargeError)
+	return ok
+}
+
+func (e *DatagramTooLargeError) Error() string { return "DATAGRAM frame too large" }
diff --git a/example/echo/echo.go b/example/echo/echo.go
index 011c70a3..7c81c826 100644
--- a/example/echo/echo.go
+++ b/example/echo/echo.go
@@ -36,14 +36,19 @@ func echoServer() error {
 	if err != nil {
 		return err
 	}
+	defer listener.Close()
+
 	conn, err := listener.Accept(context.Background())
 	if err != nil {
 		return err
 	}
+
 	stream, err := conn.AcceptStream(context.Background())
 	if err != nil {
 		panic(err)
 	}
+	defer stream.Close()
+
 	// Echo through the loggingWriter
 	_, err = io.Copy(loggingWriter{stream}, stream)
 	return err
@@ -58,11 +63,13 @@ func clientMain() error {
 	if err != nil {
 		return err
 	}
+	defer conn.CloseWithError(0, "")
 
 	stream, err := conn.OpenStreamSync(context.Background())
 	if err != nil {
 		return err
 	}
+	defer stream.Close()
 
 	fmt.Printf("Client: Sending '%s'\n", message)
 	_, err = stream.Write([]byte(message))
diff --git a/framer.go b/framer.go
index 9409af4c..d5c61bcf 100644
--- a/framer.go
+++ b/framer.go
@@ -23,6 +23,8 @@ type framer interface {
 	Handle0RTTRejection() error
 }
 
+const maxPathResponses = 256
+
 type framerI struct {
 	mutex sync.Mutex
 
@@ -33,6 +35,7 @@ type framerI struct {
 
 	controlFrameMutex sync.Mutex
 	controlFrames     []wire.Frame
+	pathResponses     []*wire.PathResponseFrame
 }
 
 var _ framer = &framerI{}
@@ -52,20 +55,43 @@ func (f *framerI) HasData() bool {
 		return true
 	}
 	f.controlFrameMutex.Lock()
-	hasData = len(f.controlFrames) > 0
-	f.controlFrameMutex.Unlock()
-	return hasData
+	defer f.controlFrameMutex.Unlock()
+	return len(f.controlFrames) > 0 || len(f.pathResponses) > 0
 }
 
 func (f *framerI) QueueControlFrame(frame wire.Frame) {
 	f.controlFrameMutex.Lock()
+	defer f.controlFrameMutex.Unlock()
+
+	if pr, ok := frame.(*wire.PathResponseFrame); ok {
+		// Only queue up to maxPathResponses PATH_RESPONSE frames.
+		// This limit should be high enough to never be hit in practice,
+		// unless the peer is doing something malicious.
+		if len(f.pathResponses) >= maxPathResponses {
+			return
+		}
+		f.pathResponses = append(f.pathResponses, pr)
+		return
+	}
 	f.controlFrames = append(f.controlFrames, frame)
-	f.controlFrameMutex.Unlock()
 }
 
 func (f *framerI) AppendControlFrames(frames []ackhandler.Frame, maxLen protocol.ByteCount, v protocol.VersionNumber) ([]ackhandler.Frame, protocol.ByteCount) {
-	var length protocol.ByteCount
 	f.controlFrameMutex.Lock()
+	defer f.controlFrameMutex.Unlock()
+
+	var length protocol.ByteCount
+	// add a PATH_RESPONSE first, but only pack a single PATH_RESPONSE per packet
+	if len(f.pathResponses) > 0 {
+		frame := f.pathResponses[0]
+		frameLen := frame.Length(v)
+		if frameLen <= maxLen {
+			frames = append(frames, ackhandler.Frame{Frame: frame})
+			length += frameLen
+			f.pathResponses = f.pathResponses[1:]
+		}
+	}
+
 	for len(f.controlFrames) > 0 {
 		frame := f.controlFrames[len(f.controlFrames)-1]
 		frameLen := frame.Length(v)
@@ -76,7 +102,6 @@ func (f *framerI) AppendControlFrames(frames []ackhandler.Frame, maxLen protocol
 		length += frameLen
 		f.controlFrames = f.controlFrames[:len(f.controlFrames)-1]
 	}
-	f.controlFrameMutex.Unlock()
 	return frames, length
 }
 
diff --git a/fuzzing/frames/fuzz.go b/fuzzing/frames/fuzz.go
index 44d2eee3..7229f488 100644
--- a/fuzzing/frames/fuzz.go
+++ b/fuzzing/frames/fuzz.go
@@ -56,22 +56,23 @@ func Fuzz(data []byte) int {
 				continue
 			}
 		}
+		validateFrame(f)
 
 		startLen := len(b)
 		parsedLen := initialLen - len(data)
 		b, err = f.Append(b, version)
 		if err != nil {
-			panic(fmt.Sprintf("Error writing frame %#v: %s", f, err))
+			panic(fmt.Sprintf("error writing frame %#v: %s", f, err))
 		}
 		frameLen := protocol.ByteCount(len(b) - startLen)
 		if f.Length(version) != frameLen {
-			panic(fmt.Sprintf("Inconsistent frame length for %#v: expected %d, got %d", f, frameLen, f.Length(version)))
+			panic(fmt.Sprintf("inconsistent frame length for %#v: expected %d, got %d", f, frameLen, f.Length(version)))
 		}
 		if sf, ok := f.(*wire.StreamFrame); ok {
 			sf.PutBack()
 		}
 		if frameLen > protocol.ByteCount(parsedLen) {
-			panic(fmt.Sprintf("Serialized length (%d) is longer than parsed length (%d)", len(b), parsedLen))
+			panic(fmt.Sprintf("serialized length (%d) is longer than parsed length (%d)", len(b), parsedLen))
 		}
 	}
 
@@ -80,3 +81,52 @@ func Fuzz(data []byte) int {
 	}
 	return 1
 }
+
+func validateFrame(frame wire.Frame) {
+	switch f := frame.(type) {
+	case *wire.StreamFrame:
+		if protocol.ByteCount(len(f.Data)) != f.DataLen() {
+			panic("STREAM frame: inconsistent data length")
+		}
+	case *wire.AckFrame:
+		if f.DelayTime < 0 {
+			panic(fmt.Sprintf("invalid ACK delay_time: %s", f.DelayTime))
+		}
+		if f.LargestAcked() < f.LowestAcked() {
+			panic("ACK: largest acknowledged is smaller than lowest acknowledged")
+		}
+		for _, r := range f.AckRanges {
+			if r.Largest < 0 || r.Smallest < 0 {
+				panic("ACK range contains a negative packet number")
+			}
+		}
+		if !f.AcksPacket(f.LargestAcked()) {
+			panic("ACK frame claims that largest acknowledged is not acknowledged")
+		}
+		if !f.AcksPacket(f.LowestAcked()) {
+			panic("ACK frame claims that lowest acknowledged is not acknowledged")
+		}
+		_ = f.AcksPacket(100)
+		_ = f.AcksPacket((f.LargestAcked() + f.LowestAcked()) / 2)
+	case *wire.NewConnectionIDFrame:
+		if f.ConnectionID.Len() < 1 || f.ConnectionID.Len() > 20 {
+			panic(fmt.Sprintf("invalid NEW_CONNECTION_ID frame length: %s", f.ConnectionID))
+		}
+	case *wire.NewTokenFrame:
+		if len(f.Token) == 0 {
+			panic("NEW_TOKEN frame with an empty token")
+		}
+	case *wire.MaxStreamsFrame:
+		if f.MaxStreamNum > protocol.MaxStreamCount {
+			panic("MAX_STREAMS frame with an invalid Maximum Streams value")
+		}
+	case *wire.StreamsBlockedFrame:
+		if f.StreamLimit > protocol.MaxStreamCount {
+			panic("STREAMS_BLOCKED frame with an invalid Maximum Streams value")
+		}
+	case *wire.ConnectionCloseFrame:
+		if f.IsApplicationError && f.FrameType != 0 {
+			panic("CONNECTION_CLOSE for an application error containing a frame type")
+		}
+	}
+}
diff --git a/fuzzing/transportparameters/fuzz.go b/fuzzing/transportparameters/fuzz.go
index a3ca143d..d0dd975f 100644
--- a/fuzzing/transportparameters/fuzz.go
+++ b/fuzzing/transportparameters/fuzz.go
@@ -2,6 +2,7 @@ package transportparameters
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 
 	"github.com/quic-go/quic-go/fuzzing/internal/helper"
@@ -26,23 +27,29 @@ func Fuzz(data []byte) int {
 	return fuzzTransportParameters(data[PrefixLen:], helper.NthBit(data[0], 1))
 }
 
-func fuzzTransportParameters(data []byte, isServer bool) int {
-	perspective := protocol.PerspectiveClient
-	if isServer {
-		perspective = protocol.PerspectiveServer
+func fuzzTransportParameters(data []byte, sentByServer bool) int {
+	sentBy := protocol.PerspectiveClient
+	if sentByServer {
+		sentBy = protocol.PerspectiveServer
 	}
 
 	tp := &wire.TransportParameters{}
-	if err := tp.Unmarshal(data, perspective); err != nil {
+	if err := tp.Unmarshal(data, sentBy); err != nil {
 		return 0
 	}
 	_ = tp.String()
+	if err := validateTransportParameters(tp, sentBy); err != nil {
+		panic(err)
+	}
 
 	tp2 := &wire.TransportParameters{}
-	if err := tp2.Unmarshal(tp.Marshal(perspective), perspective); err != nil {
+	if err := tp2.Unmarshal(tp.Marshal(sentBy), sentBy); err != nil {
 		fmt.Printf("%#v\n", tp)
 		panic(err)
 	}
+	if err := validateTransportParameters(tp2, sentBy); err != nil {
+		panic(err)
+	}
 	return 1
 }
 
@@ -58,3 +65,34 @@ func fuzzTransportParametersForSessionTicket(data []byte) int {
 	}
 	return 1
 }
+
+func validateTransportParameters(tp *wire.TransportParameters, sentBy protocol.Perspective) error {
+	if sentBy == protocol.PerspectiveClient && tp.StatelessResetToken != nil {
+		return errors.New("client's transport parameters contained stateless reset token")
+	}
+	if tp.MaxIdleTimeout < 0 {
+		return fmt.Errorf("negative max_idle_timeout: %s", tp.MaxIdleTimeout)
+	}
+	if tp.AckDelayExponent > 20 {
+		return fmt.Errorf("invalid ack_delay_exponent: %d", tp.AckDelayExponent)
+	}
+	if tp.MaxUDPPayloadSize < 1200 {
+		return fmt.Errorf("invalid max_udp_payload_size: %d", tp.MaxUDPPayloadSize)
+	}
+	if tp.ActiveConnectionIDLimit < 2 {
+		return fmt.Errorf("invalid active_connection_id_limit: %d", tp.ActiveConnectionIDLimit)
+	}
+	if tp.OriginalDestinationConnectionID.Len() > 20 {
+		return fmt.Errorf("invalid original_destination_connection_id length: %s", tp.InitialSourceConnectionID)
+	}
+	if tp.InitialSourceConnectionID.Len() > 20 {
+		return fmt.Errorf("invalid initial_source_connection_id length: %s", tp.InitialSourceConnectionID)
+	}
+	if tp.RetrySourceConnectionID != nil && tp.RetrySourceConnectionID.Len() > 20 {
+		return fmt.Errorf("invalid retry_source_connection_id length: %s", tp.RetrySourceConnectionID)
+	}
+	if tp.PreferredAddress != nil && tp.PreferredAddress.ConnectionID.Len() > 20 {
+		return fmt.Errorf("invalid preferred_address connection ID length: %s", tp.PreferredAddress.ConnectionID)
+	}
+	return nil
+}
diff --git a/http3/roundtrip.go b/http3/roundtrip.go
index bed42103..d86b0bb2 100644
--- a/http3/roundtrip.go
+++ b/http3/roundtrip.go
@@ -202,6 +202,7 @@ func (r *RoundTripper) getClient(hostname string, onlyCached bool) (rtc *roundTr
 				MaxHeaderBytes:     r.MaxResponseHeaderBytes,
 				StreamHijacker:     r.StreamHijacker,
 				UniStreamHijacker:  r.UniStreamHijacker,
+				AdditionalSettings: r.AdditionalSettings,
 			},
 			r.QuicConfig,
 			dial,
diff --git a/http3/server.go b/http3/server.go
index ac2e32a6..a66a2711 100644
--- a/http3/server.go
+++ b/http3/server.go
@@ -30,6 +30,7 @@ var (
 	quicListenAddr = func(addr string, tlsConf *tls.Config, config *quic.Config) (QUICEarlyListener, error) {
 		return quic.ListenAddrEarly(addr, tlsConf, config)
 	}
+	errPanicked = errors.New("panicked")
 )
 
 // NextProtoH3 is the ALPN protocol negotiated during the TLS handshake, for QUIC v1 and v2.
@@ -115,6 +116,16 @@ func (k *contextKey) String() string { return "quic-go/http3 context value " + k
 // type *http3.Server.
 var ServerContextKey = &contextKey{"http3-server"}
 
+// RemoteAddrContextKey is a context key. It can be used in
+// HTTP handlers with Context.Value to access the remote
+// address of the connection. The associated value will be of
+// type net.Addr.
+//
+// Use this value instead of [http.Request.RemoteAddr] if you
+// require access to the remote address of the connection rather
+// than its string representation.
+var RemoteAddrContextKey = &contextKey{"remote-addr"}
+
 type requestError struct {
 	err       error
 	streamErr ErrCode
@@ -597,6 +608,7 @@ func (s *Server) handleRequest(conn quic.Connection, str quic.Stream, decoder *q
 	ctx := str.Context()
 	ctx = context.WithValue(ctx, ServerContextKey, s)
 	ctx = context.WithValue(ctx, http.LocalAddrContextKey, conn.LocalAddr())
+	ctx = context.WithValue(ctx, RemoteAddrContextKey, conn.RemoteAddr())
 	req = req.WithContext(ctx)
 	r := newResponseWriter(str, conn, s.logger)
 	if req.Method == http.MethodHead {
@@ -641,6 +653,11 @@ func (s *Server) handleRequest(conn quic.Connection, str quic.Stream, decoder *q
 	}
 	// If the EOF was read by the handler, CancelRead() is a no-op.
 	str.CancelRead(quic.StreamErrorCode(ErrCodeNoError))
+
+	// abort the stream when there is a panic
+	if panicked {
+		return newStreamError(ErrCodeInternalError, errPanicked)
+	}
 	return requestError{}
 }
 
diff --git a/internal/handshake/crypto_setup.go b/internal/handshake/crypto_setup.go
index c5787e86..34f29887 100644
--- a/internal/handshake/crypto_setup.go
+++ b/internal/handshake/crypto_setup.go
@@ -25,7 +25,7 @@ type quicVersionContextKey struct{}
 
 var QUICVersionContextKey = &quicVersionContextKey{}
 
-const clientSessionStateRevision = 3
+const clientSessionStateRevision = 4
 
 type cryptoSetup struct {
 	tlsConf *tls.Config
@@ -313,19 +313,24 @@ func (h *cryptoSetup) handleTransportParameters(data []byte) error {
 }
 
 // must be called after receiving the transport parameters
-func (h *cryptoSetup) marshalDataForSessionState() []byte {
+func (h *cryptoSetup) marshalDataForSessionState(earlyData bool) []byte {
 	b := make([]byte, 0, 256)
 	b = quicvarint.Append(b, clientSessionStateRevision)
 	b = quicvarint.Append(b, uint64(h.rttStats.SmoothedRTT().Microseconds()))
-	return h.peerParams.MarshalForSessionTicket(b)
+	if earlyData {
+		// only save the transport parameters for 0-RTT enabled session tickets
+		return h.peerParams.MarshalForSessionTicket(b)
+	}
+	return b
 }
 
-func (h *cryptoSetup) handleDataFromSessionState(data []byte) (allowEarlyData bool) {
-	tp, err := h.handleDataFromSessionStateImpl(data)
+func (h *cryptoSetup) handleDataFromSessionState(data []byte, earlyData bool) (allowEarlyData bool) {
+	rtt, tp, err := decodeDataFromSessionState(data, earlyData)
 	if err != nil {
 		h.logger.Debugf("Restoring of transport parameters from session ticket failed: %s", err.Error())
 		return
 	}
+	h.rttStats.SetInitialRTT(rtt)
 	// The session ticket might have been saved from a connection that allowed 0-RTT,
 	// and therefore contain transport parameters.
 	// Only use them if 0-RTT is actually used on the new connection.
@@ -336,25 +341,28 @@ func (h *cryptoSetup) handleDataFromSessionState(data []byte) (allowEarlyData bo
 	return false
 }
 
-func (h *cryptoSetup) handleDataFromSessionStateImpl(data []byte) (*wire.TransportParameters, error) {
+func decodeDataFromSessionState(data []byte, earlyData bool) (time.Duration, *wire.TransportParameters, error) {
 	r := bytes.NewReader(data)
 	ver, err := quicvarint.Read(r)
 	if err != nil {
-		return nil, err
+		return 0, nil, err
 	}
 	if ver != clientSessionStateRevision {
-		return nil, fmt.Errorf("mismatching version. Got %d, expected %d", ver, clientSessionStateRevision)
+		return 0, nil, fmt.Errorf("mismatching version. Got %d, expected %d", ver, clientSessionStateRevision)
 	}
-	rtt, err := quicvarint.Read(r)
+	rttEncoded, err := quicvarint.Read(r)
 	if err != nil {
-		return nil, err
+		return 0, nil, err
+	}
+	rtt := time.Duration(rttEncoded) * time.Microsecond
+	if !earlyData {
+		return rtt, nil, nil
 	}
-	h.rttStats.SetInitialRTT(time.Duration(rtt) * time.Microsecond)
 	var tp wire.TransportParameters
 	if err := tp.UnmarshalFromSessionTicket(r); err != nil {
-		return nil, err
+		return 0, nil, err
 	}
-	return &tp, nil
+	return rtt, &tp, nil
 }
 
 func (h *cryptoSetup) getDataForSessionTicket() []byte {
diff --git a/internal/protocol/params.go b/internal/protocol/params.go
index 3ca68bf8..28b6da7c 100644
--- a/internal/protocol/params.go
+++ b/internal/protocol/params.go
@@ -129,10 +129,6 @@ const MaxPostHandshakeCryptoFrameSize = 1000
 // but must ensure that a maximum size ACK frame fits into one packet.
 const MaxAckFrameSize ByteCount = 1000
 
-// MaxDatagramFrameSize is the maximum size of a DATAGRAM frame (RFC 9221).
-// The size is chosen such that a DATAGRAM frame fits into a QUIC packet.
-const MaxDatagramFrameSize ByteCount = 1200
-
 // DatagramRcvQueueLen is the length of the receive queue for DATAGRAM frames (RFC 9221)
 const DatagramRcvQueueLen = 128
 
diff --git a/internal/qtls/client_session_cache.go b/internal/qtls/client_session_cache.go
index 336d6035..d81eb8c3 100644
--- a/internal/qtls/client_session_cache.go
+++ b/internal/qtls/client_session_cache.go
@@ -7,8 +7,8 @@ import (
 )
 
 type clientSessionCache struct {
-	getData func() []byte
-	setData func([]byte) (allowEarlyData bool)
+	getData func(earlyData bool) []byte
+	setData func(data []byte, earlyData bool) (allowEarlyData bool)
 	wrapped tls.ClientSessionCache
 }
 
@@ -24,7 +24,7 @@ func (c clientSessionCache) Put(key string, cs *tls.ClientSessionState) {
 		c.wrapped.Put(key, cs)
 		return
 	}
-	state.Extra = append(state.Extra, addExtraPrefix(c.getData()))
+	state.Extra = append(state.Extra, addExtraPrefix(c.getData(state.EarlyData)))
 	newCS, err := tls.NewResumptionState(ticket, state)
 	if err != nil {
 		// It's not clear why this would error. Just save the original state.
@@ -46,12 +46,13 @@ func (c clientSessionCache) Get(key string) (*tls.ClientSessionState, bool) {
 		c.wrapped.Put(key, nil)
 		return nil, false
 	}
-	var earlyData bool
 	// restore QUIC transport parameters and RTT stored in state.Extra
 	if extra := findExtraData(state.Extra); extra != nil {
-		earlyData = c.setData(extra)
+		earlyData := c.setData(extra, state.EarlyData)
+		if state.EarlyData {
+			state.EarlyData = earlyData
+		}
 	}
-	state.EarlyData = earlyData
 	session, err := tls.NewResumptionState(ticket, state)
 	if err != nil {
 		// It's not clear why this would error.
diff --git a/internal/qtls/go120.go b/internal/qtls/go120.go
index 7e7eee1e..554aeaf4 100644
--- a/internal/qtls/go120.go
+++ b/internal/qtls/go120.go
@@ -52,10 +52,20 @@ func SetupConfigForServer(conf *QUICConfig, enable0RTT bool, getDataForSessionTi
 	}
 }
 
-func SetupConfigForClient(conf *QUICConfig, getDataForSessionState func() []byte, setDataFromSessionState func([]byte) bool) {
+func SetupConfigForClient(
+	conf *QUICConfig,
+	getDataForSessionState func(earlyData bool) []byte,
+	setDataFromSessionState func(data []byte, earlyData bool) (allowEarlyData bool),
+) {
 	conf.ExtraConfig = &qtls.ExtraConfig{
-		GetAppDataForSessionState:  getDataForSessionState,
-		SetAppDataFromSessionState: setDataFromSessionState,
+		GetAppDataForSessionState: func() []byte {
+			// qtls only calls the GetAppDataForSessionState when doing 0-RTT
+			return getDataForSessionState(true)
+		},
+		SetAppDataFromSessionState: func(data []byte) (allowEarlyData bool) {
+			// qtls only calls the SetAppDataFromSessionState for 0-RTT enabled tickets
+			return setDataFromSessionState(data, true)
+		},
 	}
 }
 
diff --git a/internal/qtls/go121.go b/internal/qtls/go121.go
index 35a52ce0..66e289b5 100644
--- a/internal/qtls/go121.go
+++ b/internal/qtls/go121.go
@@ -93,7 +93,11 @@ func SetupConfigForServer(qconf *QUICConfig, _ bool, getData func() []byte, hand
 	}
 }
 
-func SetupConfigForClient(qconf *QUICConfig, getData func() []byte, setData func([]byte) bool) {
+func SetupConfigForClient(
+	qconf *QUICConfig,
+	getData func(earlyData bool) []byte,
+	setData func(data []byte, earlyData bool) (allowEarlyData bool),
+) {
 	conf := qconf.TLSConfig
 	if conf.ClientSessionCache != nil {
 		origCache := conf.ClientSessionCache
diff --git a/internal/wire/ack_frame.go b/internal/wire/ack_frame.go
index 9b23cc25..9ce98aee 100644
--- a/internal/wire/ack_frame.go
+++ b/internal/wire/ack_frame.go
@@ -37,7 +37,7 @@ func parseAckFrame(frame *AckFrame, r *bytes.Reader, typ uint64, ackDelayExponen
 
 	delayTime := time.Duration(delay*1<<ackDelayExponent) * time.Microsecond
 	if delayTime < 0 {
-		// If the delay time overflows, set it to the maximum encodable value.
+		// If the delay time overflows, set it to the maximum encode-able value.
 		delayTime = utils.InfDuration
 	}
 	frame.DelayTime = delayTime
@@ -57,9 +57,9 @@ func parseAckFrame(frame *AckFrame, r *bytes.Reader, typ uint64, ackDelayExponen
 		return errors.New("invalid first ACK range")
 	}
 	smallest := largestAcked - ackBlock
+	frame.AckRanges = append(frame.AckRanges, AckRange{Smallest: smallest, Largest: largestAcked})
 
 	// read all the other ACK ranges
-	frame.AckRanges = append(frame.AckRanges, AckRange{Smallest: smallest, Largest: largestAcked})
 	for i := uint64(0); i < numBlocks; i++ {
 		g, err := quicvarint.Read(r)
 		if err != nil {
diff --git a/internal/wire/datagram_frame.go b/internal/wire/datagram_frame.go
index e6c45196..4d001084 100644
--- a/internal/wire/datagram_frame.go
+++ b/internal/wire/datagram_frame.go
@@ -8,6 +8,12 @@ import (
 	"github.com/quic-go/quic-go/quicvarint"
 )
 
+// MaxDatagramSize is the maximum size of a DATAGRAM frame (RFC 9221).
+// By setting it to a large value, we allow all datagrams that fit into a QUIC packet.
+// The value is chosen such that it can still be encoded as a 2 byte varint.
+// This is a var and not a const so it can be set in tests.
+var MaxDatagramSize protocol.ByteCount = 16383
+
 // A DatagramFrame is a DATAGRAM frame
 type DatagramFrame struct {
 	DataLenPresent bool
diff --git a/internal/wire/new_connection_id_frame.go b/internal/wire/new_connection_id_frame.go
index 83102d5d..713ab996 100644
--- a/internal/wire/new_connection_id_frame.go
+++ b/internal/wire/new_connection_id_frame.go
@@ -2,6 +2,7 @@ package wire
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"io"
 
@@ -34,6 +35,9 @@ func parseNewConnectionIDFrame(r *bytes.Reader, _ protocol.VersionNumber) (*NewC
 	if err != nil {
 		return nil, err
 	}
+	if connIDLen == 0 {
+		return nil, errors.New("invalid zero-length connection ID")
+	}
 	connID, err := protocol.ReadConnectionID(r, int(connIDLen))
 	if err != nil {
 		return nil, err
diff --git a/oss-fuzz.sh b/oss-fuzz.sh
index 1efe6baa..f72fb1b0 100644
--- a/oss-fuzz.sh
+++ b/oss-fuzz.sh
@@ -3,12 +3,12 @@
 # Install Go manually, since oss-fuzz ships with an outdated Go version.
 # See https://github.com/google/oss-fuzz/pull/10643.
 export CXX="${CXX} -lresolv" # required by Go 1.20
-wget https://go.dev/dl/go1.20.5.linux-amd64.tar.gz \
+wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz \
   && mkdir temp-go \
   && rm -rf /root/.go/* \
-  && tar -C temp-go/ -xzf go1.20.5.linux-amd64.tar.gz \
+  && tar -C temp-go/ -xzf go1.21.5.linux-amd64.tar.gz \
   && mv temp-go/go/* /root/.go/ \
-  && rm -rf temp-go go1.20.5.linux-amd64.tar.gz
+  && rm -rf temp-go go1.21.5.linux-amd64.tar.gz
 
 (
 # fuzz qpack
diff --git a/packet_packer.go b/packet_packer.go
index 64081c68..a330632b 100644
--- a/packet_packer.go
+++ b/packet_packer.go
@@ -640,7 +640,13 @@ func (p *packetPacker) composeNextPacket(maxFrameSize protocol.ByteCount, onlyAc
 		pl.length += lengthAdded
 		// add handlers for the control frames that were added
 		for i := startLen; i < len(pl.frames); i++ {
-			pl.frames[i].Handler = p.retransmissionQueue.AppDataAckHandler()
+			switch pl.frames[i].Frame.(type) {
+			case *wire.PathChallengeFrame, *wire.PathResponseFrame:
+				// Path probing is currently not supported, therefore we don't need to set the OnAcked callback yet.
+				// PATH_CHALLENGE and PATH_RESPONSE are never retransmitted.
+			default:
+				pl.frames[i].Handler = p.retransmissionQueue.AppDataAckHandler()
+			}
 		}
 
 		pl.streamFrames, lengthAdded = p.framer.AppendStreamFrames(pl.streamFrames, maxFrameSize-pl.length, v)


Test patch:
diff --git a/framer_test.go b/framer_test.go
index add9da71..97cbd144 100644
--- a/framer_test.go
+++ b/framer_test.go
@@ -2,7 +2,8 @@ package quic
 
 import (
 	"bytes"
-	"math/rand"
+
+	"golang.org/x/exp/rand"
 
 	"github.com/quic-go/quic-go/internal/ackhandler"
 	"github.com/quic-go/quic-go/internal/protocol"
@@ -110,6 +111,55 @@ var _ = Describe("Framer", func() {
 		})
 	})
 
+	Context("handling PATH_RESPONSE frames", func() {
+		It("packs a single PATH_RESPONSE per packet", func() {
+			f1 := &wire.PathResponseFrame{Data: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}}
+			f2 := &wire.PathResponseFrame{Data: [8]byte{2, 3, 4, 5, 6, 7, 8, 9}}
+			cf1 := &wire.DataBlockedFrame{MaximumData: 1337}
+			cf2 := &wire.HandshakeDoneFrame{}
+			framer.QueueControlFrame(f1)
+			framer.QueueControlFrame(f2)
+			framer.QueueControlFrame(cf1)
+			framer.QueueControlFrame(cf2)
+			// the first packet should contain a single PATH_RESPONSE frame, but all the other control frames
+			Expect(framer.HasData()).To(BeTrue())
+			frames, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
+			Expect(frames).To(HaveLen(3))
+			Expect(frames[0].Frame).To(Equal(f1))
+			Expect([]wire.Frame{frames[1].Frame, frames[2].Frame}).To(ContainElement(cf1))
+			Expect([]wire.Frame{frames[1].Frame, frames[2].Frame}).To(ContainElement(cf2))
+			Expect(length).To(Equal(f1.Length(protocol.Version1) + cf1.Length(protocol.Version1) + cf2.Length(protocol.Version1)))
+			// the second packet should contain the other PATH_RESPONSE frame
+			Expect(framer.HasData()).To(BeTrue())
+			frames, length = framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
+			Expect(frames).To(HaveLen(1))
+			Expect(frames[0].Frame).To(Equal(f2))
+			Expect(length).To(Equal(f2.Length(protocol.Version1)))
+			Expect(framer.HasData()).To(BeFalse())
+		})
+
+		It("limits the number of queued PATH_RESPONSE frames", func() {
+			var pathResponses []*wire.PathResponseFrame
+			for i := 0; i < 2*maxPathResponses; i++ {
+				var f wire.PathResponseFrame
+				rand.Read(f.Data[:])
+				pathResponses = append(pathResponses, &f)
+				framer.QueueControlFrame(&f)
+			}
+			for i := 0; i < maxPathResponses; i++ {
+				Expect(framer.HasData()).To(BeTrue())
+				frames, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
+				Expect(frames).To(HaveLen(1))
+				Expect(frames[0].Frame).To(Equal(pathResponses[i]))
+				Expect(length).To(Equal(pathResponses[i].Length(protocol.Version1)))
+			}
+			Expect(framer.HasData()).To(BeFalse())
+			frames, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
+			Expect(frames).To(BeEmpty())
+			Expect(length).To(BeZero())
+		})
+	})
+
 	Context("popping STREAM frames", func() {
 		It("returns nil when popping an empty framer", func() {
 			Expect(framer.AppendStreamFrames(nil, 1000, protocol.Version1)).To(BeEmpty())
diff --git a/http3/roundtrip_test.go b/http3/roundtrip_test.go
index 30672384..8b59a8bb 100644
--- a/http3/roundtrip_test.go
+++ b/http3/roundtrip_test.go
@@ -71,6 +71,21 @@ var _ = Describe("RoundTripper", func() {
 			Expect(err).To(MatchError(testErr))
 		})
 
+		It("creates new clients with additional settings", func() {
+			testErr := errors.New("test err")
+			req, err := http.NewRequest("GET", "https://quic.clemente.io/foobar.html", nil)
+			Expect(err).ToNot(HaveOccurred())
+			rt.AdditionalSettings = map[uint64]uint64{1337: 42}
+			rt.newClient = func(_ string, _ *tls.Config, opts *roundTripperOpts, conf *quic.Config, _ dialFunc) (roundTripCloser, error) {
+				cl := NewMockRoundTripCloser(mockCtrl)
+				cl.EXPECT().RoundTripOpt(gomock.Any(), gomock.Any()).Return(nil, testErr)
+				Expect(opts.AdditionalSettings).To(HaveKeyWithValue(uint64(1337), uint64(42)))
+				return cl, nil
+			}
+			_, err = rt.RoundTrip(req)
+			Expect(err).To(MatchError(testErr))
+		})
+
 		It("uses the quic.Config, if provided", func() {
 			config := &quic.Config{HandshakeIdleTimeout: time.Millisecond}
 			var receivedConfig *quic.Config
diff --git a/http3/server_test.go b/http3/server_test.go
index 5ec58668..8da80611 100644
--- a/http3/server_test.go
+++ b/http3/server_test.go
@@ -272,7 +272,7 @@ var _ = Describe("Server", func() {
 			str.EXPECT().CancelRead(gomock.Any())
 
 			serr := s.handleRequest(conn, str, qpackDecoder, nil)
-			Expect(serr.err).ToNot(HaveOccurred())
+			Expect(serr.err).To(MatchError(errPanicked))
 			Expect(responseBuf.Bytes()).To(HaveLen(0))
 		})
 
@@ -288,7 +288,7 @@ var _ = Describe("Server", func() {
 			str.EXPECT().CancelRead(gomock.Any())
 
 			serr := s.handleRequest(conn, str, qpackDecoder, nil)
-			Expect(serr.err).ToNot(HaveOccurred())
+			Expect(serr.err).To(MatchError(errPanicked))
 			Expect(responseBuf.Bytes()).To(HaveLen(0))
 		})
 
diff --git a/integrationtests/self/datagram_test.go b/integrationtests/self/datagram_test.go
index 55e85771..f9608272 100644
--- a/integrationtests/self/datagram_test.go
+++ b/integrationtests/self/datagram_test.go
@@ -19,7 +19,8 @@ import (
 )
 
 var _ = Describe("Datagram test", func() {
-	const num = 100
+	const concurrentSends = 100
+	const maxDatagramSize = 250
 
 	var (
 		serverConn, clientConn *net.UDPConn
@@ -47,11 +48,11 @@ var _ = Describe("Datagram test", func() {
 
 			if expectDatagramSupport {
 				Expect(conn.ConnectionState().SupportsDatagrams).To(BeTrue())
-
 				if enableDatagram {
+					f := &wire.DatagramFrame{DataLenPresent: true}
 					var wg sync.WaitGroup
-					wg.Add(num)
-					for i := 0; i < num; i++ {
+					wg.Add(concurrentSends)
+					for i := 0; i < concurrentSends; i++ {
 						go func(i int) {
 							defer GinkgoRecover()
 							defer wg.Done()
@@ -60,6 +61,11 @@ var _ = Describe("Datagram test", func() {
 							Expect(conn.SendDatagram(b)).To(Succeed())
 						}(i)
 					}
+					maxDatagramMessageSize := f.MaxDataLen(maxDatagramSize, conn.ConnectionState().Version)
+					b := make([]byte, maxDatagramMessageSize+1)
+					Expect(conn.SendDatagram(b)).To(MatchError(&quic.DatagramTooLargeError{
+						PeerMaxDatagramFrameSize: int64(maxDatagramMessageSize),
+					}))
 					wg.Wait()
 				}
 			} else {
@@ -103,6 +109,8 @@ var _ = Describe("Datagram test", func() {
 	})
 
 	It("sends datagrams", func() {
+		oldMaxDatagramSize := wire.MaxDatagramSize
+		wire.MaxDatagramSize = maxDatagramSize
 		proxyPort, close := startServerAndProxy(true, true)
 		defer close()
 		raddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("localhost:%d", proxyPort))
@@ -128,14 +136,15 @@ var _ = Describe("Datagram test", func() {
 		}
 
 		numDropped := int(dropped.Load())
-		expVal := num - numDropped
+		expVal := concurrentSends - numDropped
 		fmt.Fprintf(GinkgoWriter, "Dropped %d out of %d packets.\n", numDropped, total.Load())
-		fmt.Fprintf(GinkgoWriter, "Received %d out of %d sent datagrams.\n", counter, num)
+		fmt.Fprintf(GinkgoWriter, "Received %d out of %d sent datagrams.\n", counter, concurrentSends)
 		Expect(counter).To(And(
 			BeNumerically(">", expVal*9/10),
-			BeNumerically("<", num),
+			BeNumerically("<", concurrentSends),
 		))
 		Eventually(conn.Context().Done).Should(BeClosed())
+		wire.MaxDatagramSize = oldMaxDatagramSize
 	})
 
 	It("server can disable datagram", func() {
diff --git a/integrationtests/self/go119_test.go b/integrationtests/self/go119_test.go
deleted file mode 100644
index c676693d..00000000
--- a/integrationtests/self/go119_test.go
+++ /dev/null
@@ -1,21 +0,0 @@
-//go:build go1.19 && !go1.20
-
-package self_test
-
-import (
-	"errors"
-	"net/http"
-	"time"
-)
-
-const go120 = false
-
-var errNotSupported = errors.New("not supported")
-
-func setReadDeadline(w http.ResponseWriter, deadline time.Time) error {
-	return errNotSupported
-}
-
-func setWriteDeadline(w http.ResponseWriter, deadline time.Time) error {
-	return errNotSupported
-}
diff --git a/integrationtests/self/go120_test.go b/integrationtests/self/go120_test.go
deleted file mode 100644
index 88eb4a7e..00000000
--- a/integrationtests/self/go120_test.go
+++ /dev/null
@@ -1,22 +0,0 @@
-//go:build go1.20
-
-package self_test
-
-import (
-	"net/http"
-	"time"
-)
-
-const go120 = true
-
-func setReadDeadline(w http.ResponseWriter, deadline time.Time) error {
-	rc := http.NewResponseController(w)
-
-	return rc.SetReadDeadline(deadline)
-}
-
-func setWriteDeadline(w http.ResponseWriter, deadline time.Time) error {
-	rc := http.NewResponseController(w)
-
-	return rc.SetWriteDeadline(deadline)
-}
diff --git a/integrationtests/self/http_test.go b/integrationtests/self/http_test.go
index ac16ffa5..5ec17e2d 100644
--- a/integrationtests/self/http_test.go
+++ b/integrationtests/self/http_test.go
@@ -140,6 +140,26 @@ var _ = Describe("HTTP tests", func() {
 		Expect(resp.Header.Get("Content-Length")).To(Equal(strconv.Itoa(len("foobar"))))
 	})
 
+	It("detects stream errors when server panics when writing response", func() {
+		respChan := make(chan struct{})
+		mux.HandleFunc("/writing_and_panicking", func(w http.ResponseWriter, r *http.Request) {
+			// no recover here as it will interfere with the handler
+			w.Write([]byte("foobar"))
+			w.(http.Flusher).Flush()
+			// wait for the client to receive the response
+			<-respChan
+			panic(http.ErrAbortHandler)
+		})
+
+		resp, err := client.Get(fmt.Sprintf("https://localhost:%d/writing_and_panicking", port))
+		close(respChan)
+		Expect(err).ToNot(HaveOccurred())
+		body, err := io.ReadAll(resp.Body)
+		Expect(err).To(HaveOccurred())
+		// the body will be a prefix of what's written
+		Expect(bytes.HasPrefix([]byte("foobar"), body)).To(BeTrue())
+	})
+
 	It("requests to different servers with the same udpconn", func() {
 		resp, err := client.Get(fmt.Sprintf("https://localhost:%d/remoteAddr", port))
 		Expect(err).ToNot(HaveOccurred())
@@ -432,55 +452,65 @@ var _ = Describe("HTTP tests", func() {
 		Eventually(done).Should(BeClosed())
 	})
 
-	if go120 {
-		It("supports read deadlines", func() {
-			mux.HandleFunc("/read-deadline", func(w http.ResponseWriter, r *http.Request) {
-				defer GinkgoRecover()
-				err := setReadDeadline(w, time.Now().Add(deadlineDelay))
-				Expect(err).ToNot(HaveOccurred())
+	It("supports read deadlines", func() {
+		mux.HandleFunc("/read-deadline", func(w http.ResponseWriter, r *http.Request) {
+			defer GinkgoRecover()
+			rc := http.NewResponseController(w)
+			Expect(rc.SetReadDeadline(time.Now().Add(deadlineDelay))).To(Succeed())
 
-				body, err := io.ReadAll(r.Body)
-				Expect(err).To(MatchError(os.ErrDeadlineExceeded))
-				Expect(body).To(ContainSubstring("aa"))
+			body, err := io.ReadAll(r.Body)
+			Expect(err).To(MatchError(os.ErrDeadlineExceeded))
+			Expect(body).To(ContainSubstring("aa"))
 
-				w.Write([]byte("ok"))
-			})
+			w.Write([]byte("ok"))
+		})
 
-			expectedEnd := time.Now().Add(deadlineDelay)
-			resp, err := client.Post(
-				fmt.Sprintf("https://localhost:%d/read-deadline", port),
-				"text/plain",
-				neverEnding('a'),
-			)
-			Expect(err).ToNot(HaveOccurred())
-			Expect(resp.StatusCode).To(Equal(200))
+		expectedEnd := time.Now().Add(deadlineDelay)
+		resp, err := client.Post(
+			fmt.Sprintf("https://localhost:%d/read-deadline", port),
+			"text/plain",
+			neverEnding('a'),
+		)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(resp.StatusCode).To(Equal(200))
 
-			body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 2*deadlineDelay))
-			Expect(err).ToNot(HaveOccurred())
-			Expect(time.Now().After(expectedEnd)).To(BeTrue())
-			Expect(string(body)).To(Equal("ok"))
-		})
+		body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 2*deadlineDelay))
+		Expect(err).ToNot(HaveOccurred())
+		Expect(time.Now().After(expectedEnd)).To(BeTrue())
+		Expect(string(body)).To(Equal("ok"))
+	})
 
-		It("supports write deadlines", func() {
-			mux.HandleFunc("/write-deadline", func(w http.ResponseWriter, r *http.Request) {
-				defer GinkgoRecover()
-				err := setWriteDeadline(w, time.Now().Add(deadlineDelay))
-				Expect(err).ToNot(HaveOccurred())
+	It("supports write deadlines", func() {
+		mux.HandleFunc("/write-deadline", func(w http.ResponseWriter, r *http.Request) {
+			defer GinkgoRecover()
+			rc := http.NewResponseController(w)
+			Expect(rc.SetWriteDeadline(time.Now().Add(deadlineDelay))).To(Succeed())
 
-				_, err = io.Copy(w, neverEnding('a'))
-				Expect(err).To(MatchError(os.ErrDeadlineExceeded))
-			})
+			_, err := io.Copy(w, neverEnding('a'))
+			Expect(err).To(MatchError(os.ErrDeadlineExceeded))
+		})
 
-			expectedEnd := time.Now().Add(deadlineDelay)
+		expectedEnd := time.Now().Add(deadlineDelay)
 
-			resp, err := client.Get(fmt.Sprintf("https://localhost:%d/write-deadline", port))
-			Expect(err).ToNot(HaveOccurred())
-			Expect(resp.StatusCode).To(Equal(200))
+		resp, err := client.Get(fmt.Sprintf("https://localhost:%d/write-deadline", port))
+		Expect(err).ToNot(HaveOccurred())
+		Expect(resp.StatusCode).To(Equal(200))
 
-			body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 2*deadlineDelay))
-			Expect(err).ToNot(HaveOccurred())
-			Expect(time.Now().After(expectedEnd)).To(BeTrue())
-			Expect(string(body)).To(ContainSubstring("aa"))
+		body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 2*deadlineDelay))
+		Expect(err).ToNot(HaveOccurred())
+		Expect(time.Now().After(expectedEnd)).To(BeTrue())
+		Expect(string(body)).To(ContainSubstring("aa"))
+	})
+
+	It("sets remote address", func() {
+		mux.HandleFunc("/remote-addr", func(w http.ResponseWriter, r *http.Request) {
+			defer GinkgoRecover()
+			_, ok := r.Context().Value(http3.RemoteAddrContextKey).(net.Addr)
+			Expect(ok).To(BeTrue())
 		})
-	}
+
+		resp, err := client.Get(fmt.Sprintf("https://localhost:%d/remote-addr", port))
+		Expect(err).ToNot(HaveOccurred())
+		Expect(resp.StatusCode).To(Equal(200))
+	})
 })
diff --git a/integrationtests/self/zero_rtt_oldgo_test.go b/integrationtests/self/zero_rtt_oldgo_test.go
index aea54271..af5400d1 100644
--- a/integrationtests/self/zero_rtt_oldgo_test.go
+++ b/integrationtests/self/zero_rtt_oldgo_test.go
@@ -641,6 +641,49 @@ var _ = Describe("0-RTT", func() {
 		Expect(get0RTTPackets(counter.getRcvdLongHeaderPackets())).To(BeEmpty())
 	})
 
+	It("doesn't use 0-RTT, if the server didn't enable it", func() {
+		server, err := quic.ListenAddr("localhost:0", getTLSConfig(), getQuicConfig(nil))
+		Expect(err).ToNot(HaveOccurred())
+		defer server.Close()
+
+		gets := make(chan string, 100)
+		puts := make(chan string, 100)
+		cache := newClientSessionCache(tls.NewLRUClientSessionCache(10), gets, puts)
+		tlsConf := getTLSClientConfig()
+		tlsConf.ClientSessionCache = cache
+		conn1, err := quic.DialAddr(
+			context.Background(),
+			fmt.Sprintf("localhost:%d", server.Addr().(*net.UDPAddr).Port),
+			tlsConf,
+			getQuicConfig(nil),
+		)
+		Expect(err).ToNot(HaveOccurred())
+		defer conn1.CloseWithError(0, "")
+		var sessionKey string
+		Eventually(puts).Should(Receive(&sessionKey))
+		Expect(conn1.ConnectionState().TLS.DidResume).To(BeFalse())
+
+		serverConn, err := server.Accept(context.Background())
+		Expect(err).ToNot(HaveOccurred())
+		Expect(serverConn.ConnectionState().TLS.DidResume).To(BeFalse())
+
+		conn2, err := quic.DialAddrEarly(
+			context.Background(),
+			fmt.Sprintf("localhost:%d", server.Addr().(*net.UDPAddr).Port),
+			tlsConf,
+			getQuicConfig(nil),
+		)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(gets).To(Receive(Equal(sessionKey)))
+		Expect(conn2.ConnectionState().TLS.DidResume).To(BeTrue())
+
+		serverConn, err = server.Accept(context.Background())
+		Expect(err).ToNot(HaveOccurred())
+		Expect(serverConn.ConnectionState().TLS.DidResume).To(BeTrue())
+		Expect(serverConn.ConnectionState().Used0RTT).To(BeFalse())
+		conn2.CloseWithError(0, "")
+	})
+
 	DescribeTable("flow control limits",
 		func(addFlowControlLimit func(*quic.Config, uint64)) {
 			counter, tracer := newPacketTracer()
diff --git a/integrationtests/self/zero_rtt_test.go b/integrationtests/self/zero_rtt_test.go
index c0fc5325..7bfa66ce 100644
--- a/integrationtests/self/zero_rtt_test.go
+++ b/integrationtests/self/zero_rtt_test.go
@@ -692,6 +692,49 @@ var _ = Describe("0-RTT", func() {
 		Expect(get0RTTPackets(counter.getRcvdLongHeaderPackets())).To(BeEmpty())
 	})
 
+	It("doesn't use 0-RTT, if the server didn't enable it", func() {
+		server, err := quic.ListenAddr("localhost:0", getTLSConfig(), getQuicConfig(nil))
+		Expect(err).ToNot(HaveOccurred())
+		defer server.Close()
+
+		gets := make(chan string, 100)
+		puts := make(chan string, 100)
+		cache := newClientSessionCache(tls.NewLRUClientSessionCache(10), gets, puts)
+		tlsConf := getTLSClientConfig()
+		tlsConf.ClientSessionCache = cache
+		conn1, err := quic.DialAddr(
+			context.Background(),
+			fmt.Sprintf("localhost:%d", server.Addr().(*net.UDPAddr).Port),
+			tlsConf,
+			getQuicConfig(nil),
+		)
+		Expect(err).ToNot(HaveOccurred())
+		defer conn1.CloseWithError(0, "")
+		var sessionKey string
+		Eventually(puts).Should(Receive(&sessionKey))
+		Expect(conn1.ConnectionState().TLS.DidResume).To(BeFalse())
+
+		serverConn, err := server.Accept(context.Background())
+		Expect(err).ToNot(HaveOccurred())
+		Expect(serverConn.ConnectionState().TLS.DidResume).To(BeFalse())
+
+		conn2, err := quic.DialAddrEarly(
+			context.Background(),
+			fmt.Sprintf("localhost:%d", server.Addr().(*net.UDPAddr).Port),
+			tlsConf,
+			getQuicConfig(nil),
+		)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(gets).To(Receive(Equal(sessionKey)))
+		Expect(conn2.ConnectionState().TLS.DidResume).To(BeTrue())
+
+		serverConn, err = server.Accept(context.Background())
+		Expect(err).ToNot(HaveOccurred())
+		Expect(serverConn.ConnectionState().TLS.DidResume).To(BeTrue())
+		Expect(serverConn.ConnectionState().Used0RTT).To(BeFalse())
+		conn2.CloseWithError(0, "")
+	})
+
 	DescribeTable("flow control limits",
 		func(addFlowControlLimit func(*quic.Config, uint64)) {
 			counter, tracer := newPacketTracer()
diff --git a/internal/qtls/client_session_cache_test.go b/internal/qtls/client_session_cache_test.go
index 6af19293..fdb0aa06 100644
--- a/internal/qtls/client_session_cache_test.go
+++ b/internal/qtls/client_session_cache_test.go
@@ -40,8 +40,9 @@ var _ = Describe("Client Session Cache", func() {
 			RootCAs: testdata.GetRootCA(),
 			ClientSessionCache: &clientSessionCache{
 				wrapped: tls.NewLRUClientSessionCache(10),
-				getData: func() []byte { return []byte("session") },
-				setData: func(data []byte) bool {
+				getData: func(bool) []byte { return []byte("session") },
+				setData: func(data []byte, earlyData bool) bool {
+					Expect(earlyData).To(BeFalse()) // running on top of TCP, we can only test non-0-RTT here
 					restored <- data
 					return true
 				},
diff --git a/internal/wire/new_connection_id_frame_test.go b/internal/wire/new_connection_id_frame_test.go
index 4628aaf2..02ef3000 100644
--- a/internal/wire/new_connection_id_frame_test.go
+++ b/internal/wire/new_connection_id_frame_test.go
@@ -38,7 +38,15 @@ var _ = Describe("NEW_CONNECTION_ID frame", func() {
 			Expect(err).To(MatchError("Retire Prior To value (1001) larger than Sequence Number (1000)"))
 		})
 
-		It("errors when the connection ID has an invalid length", func() {
+		It("errors when the connection ID has a zero-length connection ID", func() {
+			data := encodeVarInt(42)                 // sequence number
+			data = append(data, encodeVarInt(12)...) // retire prior to
+			data = append(data, 0)                   // connection ID length
+			_, err := parseNewConnectionIDFrame(bytes.NewReader(data), protocol.Version1)
+			Expect(err).To(MatchError("invalid zero-length connection ID"))
+		})
+
+		It("errors when the connection ID has an invalid length (too long)", func() {
 			data := encodeVarInt(0xdeadbeef)                                                                          // sequence number
 			data = append(data, encodeVarInt(0xcafe)...)                                                              // retire prior to
 			data = append(data, 21)                                                                                   // connection ID length
diff --git a/internal/wire/transport_parameter_test.go b/internal/wire/transport_parameter_test.go
index 2fb79539..a82b6dcf 100644
--- a/internal/wire/transport_parameter_test.go
+++ b/internal/wire/transport_parameter_test.go
@@ -503,7 +503,7 @@ var _ = Describe("Transport Parameters", func() {
 				MaxBidiStreamNum:               protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
 				MaxUniStreamNum:                protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
 				ActiveConnectionIDLimit:        2 + getRandomValueUpTo(math.MaxInt64-2),
-				MaxDatagramFrameSize:           protocol.ByteCount(getRandomValueUpTo(int64(protocol.MaxDatagramFrameSize))),
+				MaxDatagramFrameSize:           protocol.ByteCount(getRandomValueUpTo(int64(MaxDatagramSize))),
 			}
 			Expect(params.ValidFor0RTT(params)).To(BeTrue())
 			b := params.MarshalForSessionTicket(nil)
diff --git a/packet_packer_test.go b/packet_packer_test.go
index e68906f3..fe746e63 100644
--- a/packet_packer_test.go
+++ b/packet_packer_test.go
@@ -562,6 +562,37 @@ var _ = Describe("Packet packer", func() {
 				Expect(buffer.Len()).ToNot(BeZero())
 			})
 
+			It("packs PATH_CHALLENGE and PATH_RESPONSE frames", func() {
+				pnManager.EXPECT().PeekPacketNumber(protocol.Encryption1RTT).Return(protocol.PacketNumber(0x42), protocol.PacketNumberLen2)
+				pnManager.EXPECT().PopPacketNumber(protocol.Encryption1RTT).Return(protocol.PacketNumber(0x42))
+				sealingManager.EXPECT().Get1RTTSealer().Return(getSealer(), nil)
+				framer.EXPECT().HasData().Return(true)
+				ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, false)
+				frames := []ackhandler.Frame{
+					{Frame: &wire.PathChallengeFrame{}},
+					{Frame: &wire.PathResponseFrame{}},
+					{Frame: &wire.DataBlockedFrame{}},
+				}
+				expectAppendControlFrames(frames...)
+				expectAppendStreamFrames()
+				buffer := getPacketBuffer()
+				p, err := packer.AppendPacket(buffer, maxPacketSize, protocol.Version1)
+				Expect(p).ToNot(BeNil())
+				Expect(err).ToNot(HaveOccurred())
+				Expect(p.Frames).To(HaveLen(3))
+				for i, f := range p.Frames {
+					Expect(f).To(BeAssignableToTypeOf(frames[i]))
+					switch f.Frame.(type) {
+					case *wire.PathChallengeFrame, *wire.PathResponseFrame:
+						// This means that the frame won't be retransmitted.
+						Expect(f.Handler).To(BeNil())
+					default:
+						Expect(f.Handler).ToNot(BeNil())
+					}
+				}
+				Expect(buffer.Len()).ToNot(BeZero())
+			})
+
 			It("packs DATAGRAM frames", func() {
 				ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, true)
 				pnManager.EXPECT().PeekPacketNumber(protocol.Encryption1RTT).Return(protocol.PacketNumber(0x42), protocol.PacketNumberLen2)