Repository: quic-go/quic-go
Pull request #4117 (2023-10-23 02:46:55)

README: link to webtransport-go repo

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/quic-go/quic-go/pull/4117?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#4117](https://app.codecov.io/gh/quic-go/quic-go/pull/4117?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (97ac299) into [master](https://app.codecov.io/gh/quic-go/quic-go/commit/36f7fe7d07edb94f9be148494659184546b9dbcd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (36f7fe7) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##           master    #4117   +/-   ##
=======================================
  Coverage   83.74%   83.74%           
=======================================
  Files         150      150           
  Lines       15421    15421           
=======================================
  Hits        12913    12913           
  Misses       2010     2010           
  Partials      498      498           
```





Patch:
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index c661e09c..6f9b27fe 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -13,6 +13,9 @@ jobs:
           - os: "macos"
             go: "1.21.x"
     runs-on: ${{ fromJSON(vars[format('INTEGRATION_RUNNER_{0}', matrix.os)] || format('"{0}-latest"', matrix.os)) }}
+    defaults:
+      run:
+        shell: bash # by default Windows uses PowerShell, which uses a different syntax for setting environment variables
     env:
       DEBUG: false # set this to true to export qlogs and save them as artifacts
       TIMESCALE_FACTOR: 3
@@ -56,7 +59,8 @@ jobs:
           go run github.com/onsi/ginkgo/v2/ginkgo -r -v -randomize-all -randomize-suites -trace integrationtests/self -- ${{ env.QLOGFLAG }}
       - name: save qlogs
         if: ${{ always() && env.DEBUG == 'true' }}
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
-          name: qlogs
+          name: qlogs-${{ matrix.os }}-go${{ matrix.go }}
           path: integrationtests/self/*.qlog
+          retention-days: 7
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 1d564861..fd735c9b 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -8,7 +8,7 @@ jobs:
       - uses: actions/setup-go@v4
         with:
           skip-pkg-cache: true
-          go-version: "1.20.x"
+          go-version: "1.21.x"
       - name: Check that no non-test files import Ginkgo or Gomega
         run: .github/workflows/no_ginkgo.sh
       - name: Check for //go:build ignore in .go files
@@ -36,11 +36,16 @@ jobs:
           go mod vendor
   golangci-lint:
     runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        go: [ "1.20.x", "1.21.x" ]
+    name: golangci-lint (Go ${{ matrix.go }})
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-go@v4
         with:
-          go-version: "1.20.x"
+          go-version: ${{ matrix.go }}
       - name: golangci-lint (Linux)
         uses: golangci/golangci-lint-action@v3
         with:
diff --git a/README.md b/README.md
index e6beb8aa..a899f718 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,8 @@ In addition to these base RFCs, it also implements the following RFCs:
 * QUIC Version 2 ([RFC 9369](https://datatracker.ietf.org/doc/html/rfc9369))
 * QUIC Event Logging using qlog ([draft-ietf-quic-qlog-main-schema](https://datatracker.ietf.org/doc/draft-ietf-quic-qlog-main-schema/) and [draft-ietf-quic-qlog-quic-events](https://datatracker.ietf.org/doc/draft-ietf-quic-qlog-quic-events/))
 
+Support for WebTransport over HTTP/3 ([draft-ietf-webtrans-http3](https://datatracker.ietf.org/doc/draft-ietf-webtrans-http3/)) is implemented in [webtransport-go](https://github.com/quic-go/webtransport-go).
+
 ## Using QUIC
 
 ### Running a Server
@@ -159,18 +161,18 @@ On the receiver side, this is surfaced as a `quic.ApplicationError`.
 
 Unreliable datagrams are a QUIC extension ([RFC 9221](https://datatracker.ietf.org/doc/html/rfc9221)) that is negotiated during the handshake. Support can be enabled by setting the `quic.Config.EnableDatagram` flag. Note that this doesn't guarantee that the peer also supports datagrams. Whether or not the feature negotiation succeeded can be learned from the `quic.ConnectionState.SupportsDatagrams` obtained from `quic.Connection.ConnectionState()`.
 
-QUIC DATAGRAMs are a new QUIC frame type sent in QUIC 1-RTT packets (i.e. after completion of the handshake). Therefore, they're end-to-end encrypted and congestion-controlled. However, if a DATAGRAM frame is deemed lost by QUIC's loss detection mechanism, they are not automatically retransmitted.
+QUIC DATAGRAMs are a new QUIC frame type sent in QUIC 1-RTT packets (i.e. after completion of the handshake). Therefore, they're end-to-end encrypted and congestion-controlled. However, if a DATAGRAM frame is deemed lost by QUIC's loss detection mechanism, they are not retransmitted.
 
-Datagrams are sent using the `SendMessage` method on the `quic.Connection`:
+Datagrams are sent using the `SendDatagram` method on the `quic.Connection`:
 
 ```go
-conn.SendMessage([]byte("foobar"))
+conn.SendDatagram([]byte("foobar"))
 ```
 
-And received using `ReceiveMessage`:
+And received using `ReceiveDatagram`:
 
 ```go
-msg, err := conn.ReceiveMessage()
+msg, err := conn.ReceiveDatagram()
 ```
 
 Note that this code path is currently not optimized. It works for datagrams that are sent occasionally, but it doesn't achieve the same throughput as writing data on a stream. Please get in touch on issue #3766 if your use case relies on high datagram throughput, or if you'd like to help fix this issue. There are also some restrictions regarding the maximum message size (see #3599).
diff --git a/connection.go b/connection.go
index 09b522a9..93c15913 100644
--- a/connection.go
+++ b/connection.go
@@ -2343,7 +2343,7 @@ func (s *connection) onStreamCompleted(id protocol.StreamID) {
 	}
 }
 
-func (s *connection) SendMessage(p []byte) error {
+func (s *connection) SendDatagram(p []byte) error {
 	if !s.supportsDatagrams() {
 		return errors.New("datagram support disabled")
 	}
@@ -2357,7 +2357,7 @@ func (s *connection) SendMessage(p []byte) error {
 	return s.datagramQueue.AddAndWait(f)
 }
 
-func (s *connection) ReceiveMessage(ctx context.Context) ([]byte, error) {
+func (s *connection) ReceiveDatagram(ctx context.Context) ([]byte, error) {
 	if !s.config.EnableDatagrams {
 		return nil, errors.New("datagram support disabled")
 	}
diff --git a/go.mod b/go.mod
index 0a563463..c2a45a2c 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/onsi/ginkgo/v2 v2.9.5
 	github.com/onsi/gomega v1.27.6
 	github.com/quic-go/qpack v0.4.0
-	github.com/quic-go/qtls-go1-20 v0.3.4
+	github.com/quic-go/qtls-go1-20 v0.4.1
 	go.uber.org/mock v0.3.0
 	golang.org/x/crypto v0.4.0
 	golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
diff --git a/go.sum b/go.sum
index 62b640db..01249e3c 100644
--- a/go.sum
+++ b/go.sum
@@ -88,8 +88,8 @@ github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7q
 github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
 github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
-github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg=
-github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
+github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
+github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
diff --git a/interface.go b/interface.go
index 6eac385d..da0e5e2b 100644
--- a/interface.go
+++ b/interface.go
@@ -187,10 +187,10 @@ type Connection interface {
 	// Warning: This API should not be considered stable and might change soon.
 	ConnectionState() ConnectionState
 
-	// SendMessage sends a message as a datagram, as specified in RFC 9221.
-	SendMessage([]byte) error
-	// ReceiveMessage gets a message received in a datagram, as specified in RFC 9221.
-	ReceiveMessage(context.Context) ([]byte, error)
+	// SendDatagram sends a message as a datagram, as specified in RFC 9221.
+	SendDatagram([]byte) error
+	// ReceiveDatagram gets a message received in a datagram, as specified in RFC 9221.
+	ReceiveDatagram(context.Context) ([]byte, error)
 }
 
 // An EarlyConnection is a connection that is handshaking.
@@ -338,7 +338,7 @@ type ConnectionState struct {
 	// SupportsDatagrams says if support for QUIC datagrams (RFC 9221) was negotiated.
 	// This requires both nodes to support and enable the datagram extensions (via Config.EnableDatagrams).
 	// If datagram support was negotiated, datagrams can be sent and received using the
-	// SendMessage and ReceiveMessage methods on the Connection.
+	// SendDatagram and ReceiveDatagram methods on the Connection.
 	SupportsDatagrams bool
 	// Used0RTT says if 0-RTT resumption was used.
 	Used0RTT bool
diff --git a/internal/handshake/crypto_setup.go b/internal/handshake/crypto_setup.go
index cae02873..c5787e86 100644
--- a/internal/handshake/crypto_setup.go
+++ b/internal/handshake/crypto_setup.go
@@ -96,6 +96,7 @@ func NewCryptoSetupClient(
 	quicConf := &qtls.QUICConfig{TLSConfig: tlsConf}
 	qtls.SetupConfigForClient(quicConf, cs.marshalDataForSessionState, cs.handleDataFromSessionState)
 	cs.tlsConf = tlsConf
+	cs.allow0RTT = enable0RTT
 
 	cs.conn = qtls.QUICClient(quicConf)
 	cs.conn.SetTransportParameters(cs.ourParams.Marshal(protocol.PerspectiveClient))
@@ -146,6 +147,9 @@ func addConnToClientHelloInfo(conf *tls.Config, localAddr, remoteAddr net.Addr)
 			info.Conn = &conn{localAddr: localAddr, remoteAddr: remoteAddr}
 			c, err := gcfc(info)
 			if c != nil {
+				c = c.Clone()
+				// This won't be necessary anymore once https://github.com/golang/go/issues/63722 is accepted.
+				c.MinVersion = tls.VersionTLS13
 				// We're returning a tls.Config here, so we need to apply this recursively.
 				addConnToClientHelloInfo(c, localAddr, remoteAddr)
 			}
@@ -316,13 +320,20 @@ func (h *cryptoSetup) marshalDataForSessionState() []byte {
 	return h.peerParams.MarshalForSessionTicket(b)
 }
 
-func (h *cryptoSetup) handleDataFromSessionState(data []byte) {
+func (h *cryptoSetup) handleDataFromSessionState(data []byte) (allowEarlyData bool) {
 	tp, err := h.handleDataFromSessionStateImpl(data)
 	if err != nil {
 		h.logger.Debugf("Restoring of transport parameters from session ticket failed: %s", err.Error())
 		return
 	}
-	h.zeroRTTParameters = tp
+	// 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.
+	if tp != nil && h.allow0RTT {
+		h.zeroRTTParameters = tp
+		return true
+	}
+	return false
 }
 
 func (h *cryptoSetup) handleDataFromSessionStateImpl(data []byte) (*wire.TransportParameters, error) {
@@ -383,7 +394,9 @@ func (h *cryptoSetup) GetSessionTicket() ([]byte, error) {
 }
 
 // handleSessionTicket is called for the server when receiving the client's session ticket.
-// It reads parameters from the session ticket and decides whether to accept 0-RTT when the session ticket is used for 0-RTT.
+// It reads parameters from the session ticket and checks whether to accept 0-RTT if the session ticket enabled 0-RTT.
+// Note that the fact that the session ticket allows 0-RTT doesn't mean that the actual TLS handshake enables 0-RTT:
+// A client may use a 0-RTT enabled session to resume a TLS session without using 0-RTT.
 func (h *cryptoSetup) handleSessionTicket(sessionTicketData []byte, using0RTT bool) bool {
 	var t sessionTicket
 	if err := t.Unmarshal(sessionTicketData, using0RTT); err != nil {
diff --git a/internal/mocks/quic/early_conn.go b/internal/mocks/quic/early_conn.go
index 223def6c..2e726a82 100644
--- a/internal/mocks/quic/early_conn.go
+++ b/internal/mocks/quic/early_conn.go
@@ -503,41 +503,41 @@ func (c *EarlyConnectionOpenUniStreamSyncCall) DoAndReturn(f func(context.Contex
 	return c
 }
 
-// ReceiveMessage mocks base method.
-func (m *MockEarlyConnection) ReceiveMessage(arg0 context.Context) ([]byte, error) {
+// ReceiveDatagram mocks base method.
+func (m *MockEarlyConnection) ReceiveDatagram(arg0 context.Context) ([]byte, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ReceiveMessage", arg0)
+	ret := m.ctrl.Call(m, "ReceiveDatagram", arg0)
 	ret0, _ := ret[0].([]byte)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// ReceiveMessage indicates an expected call of ReceiveMessage.
-func (mr *MockEarlyConnectionMockRecorder) ReceiveMessage(arg0 any) *EarlyConnectionReceiveMessageCall {
+// ReceiveDatagram indicates an expected call of ReceiveDatagram.
+func (mr *MockEarlyConnectionMockRecorder) ReceiveDatagram(arg0 any) *EarlyConnectionReceiveDatagramCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceiveMessage", reflect.TypeOf((*MockEarlyConnection)(nil).ReceiveMessage), arg0)
-	return &EarlyConnectionReceiveMessageCall{Call: call}
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceiveDatagram", reflect.TypeOf((*MockEarlyConnection)(nil).ReceiveDatagram), arg0)
+	return &EarlyConnectionReceiveDatagramCall{Call: call}
 }
 
-// EarlyConnectionReceiveMessageCall wrap *gomock.Call
-type EarlyConnectionReceiveMessageCall struct {
+// EarlyConnectionReceiveDatagramCall wrap *gomock.Call
+type EarlyConnectionReceiveDatagramCall struct {
 	*gomock.Call
 }
 
 // Return rewrite *gomock.Call.Return
-func (c *EarlyConnectionReceiveMessageCall) Return(arg0 []byte, arg1 error) *EarlyConnectionReceiveMessageCall {
+func (c *EarlyConnectionReceiveDatagramCall) Return(arg0 []byte, arg1 error) *EarlyConnectionReceiveDatagramCall {
 	c.Call = c.Call.Return(arg0, arg1)
 	return c
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *EarlyConnectionReceiveMessageCall) Do(f func(context.Context) ([]byte, error)) *EarlyConnectionReceiveMessageCall {
+func (c *EarlyConnectionReceiveDatagramCall) Do(f func(context.Context) ([]byte, error)) *EarlyConnectionReceiveDatagramCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *EarlyConnectionReceiveMessageCall) DoAndReturn(f func(context.Context) ([]byte, error)) *EarlyConnectionReceiveMessageCall {
+func (c *EarlyConnectionReceiveDatagramCall) DoAndReturn(f func(context.Context) ([]byte, error)) *EarlyConnectionReceiveDatagramCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
@@ -580,40 +580,40 @@ func (c *EarlyConnectionRemoteAddrCall) DoAndReturn(f func() net.Addr) *EarlyCon
 	return c
 }
 
-// SendMessage mocks base method.
-func (m *MockEarlyConnection) SendMessage(arg0 []byte) error {
+// SendDatagram mocks base method.
+func (m *MockEarlyConnection) SendDatagram(arg0 []byte) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "SendMessage", arg0)
+	ret := m.ctrl.Call(m, "SendDatagram", arg0)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// SendMessage indicates an expected call of SendMessage.
-func (mr *MockEarlyConnectionMockRecorder) SendMessage(arg0 any) *EarlyConnectionSendMessageCall {
+// SendDatagram indicates an expected call of SendDatagram.
+func (mr *MockEarlyConnectionMockRecorder) SendDatagram(arg0 any) *EarlyConnectionSendDatagramCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockEarlyConnection)(nil).SendMessage), arg0)
-	return &EarlyConnectionSendMessageCall{Call: call}
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendDatagram", reflect.TypeOf((*MockEarlyConnection)(nil).SendDatagram), arg0)
+	return &EarlyConnectionSendDatagramCall{Call: call}
 }
 
-// EarlyConnectionSendMessageCall wrap *gomock.Call
-type EarlyConnectionSendMessageCall struct {
+// EarlyConnectionSendDatagramCall wrap *gomock.Call
+type EarlyConnectionSendDatagramCall struct {
 	*gomock.Call
 }
 
 // Return rewrite *gomock.Call.Return
-func (c *EarlyConnectionSendMessageCall) Return(arg0 error) *EarlyConnectionSendMessageCall {
+func (c *EarlyConnectionSendDatagramCall) Return(arg0 error) *EarlyConnectionSendDatagramCall {
 	c.Call = c.Call.Return(arg0)
 	return c
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *EarlyConnectionSendMessageCall) Do(f func([]byte) error) *EarlyConnectionSendMessageCall {
+func (c *EarlyConnectionSendDatagramCall) Do(f func([]byte) error) *EarlyConnectionSendDatagramCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *EarlyConnectionSendMessageCall) DoAndReturn(f func([]byte) error) *EarlyConnectionSendMessageCall {
+func (c *EarlyConnectionSendDatagramCall) DoAndReturn(f func([]byte) error) *EarlyConnectionSendDatagramCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/internal/qtls/client_session_cache.go b/internal/qtls/client_session_cache.go
index 519895ee..336d6035 100644
--- a/internal/qtls/client_session_cache.go
+++ b/internal/qtls/client_session_cache.go
@@ -8,7 +8,7 @@ import (
 
 type clientSessionCache struct {
 	getData func() []byte
-	setData func([]byte)
+	setData func([]byte) (allowEarlyData bool)
 	wrapped tls.ClientSessionCache
 }
 
@@ -46,10 +46,12 @@ 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 {
-		c.setData(extra)
+		earlyData = c.setData(extra)
 	}
+	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 898f0352..7e7eee1e 100644
--- a/internal/qtls/go120.go
+++ b/internal/qtls/go120.go
@@ -52,7 +52,7 @@ func SetupConfigForServer(conf *QUICConfig, enable0RTT bool, getDataForSessionTi
 	}
 }
 
-func SetupConfigForClient(conf *QUICConfig, getDataForSessionState func() []byte, setDataFromSessionState func([]byte)) {
+func SetupConfigForClient(conf *QUICConfig, getDataForSessionState func() []byte, setDataFromSessionState func([]byte) bool) {
 	conf.ExtraConfig = &qtls.ExtraConfig{
 		GetAppDataForSessionState:  getDataForSessionState,
 		SetAppDataFromSessionState: setDataFromSessionState,
diff --git a/internal/qtls/go121.go b/internal/qtls/go121.go
index 65a274ac..35a52ce0 100644
--- a/internal/qtls/go121.go
+++ b/internal/qtls/go121.go
@@ -93,7 +93,7 @@ func SetupConfigForServer(qconf *QUICConfig, _ bool, getData func() []byte, hand
 	}
 }
 
-func SetupConfigForClient(qconf *QUICConfig, getData func() []byte, setData func([]byte)) {
+func SetupConfigForClient(qconf *QUICConfig, getData func() []byte, setData func([]byte) bool) {
 	conf := qconf.TLSConfig
 	if conf.ClientSessionCache != nil {
 		origCache := conf.ClientSessionCache
diff --git a/packet_handler_map.go b/packet_handler_map.go
index 006eadf9..ba62b1e5 100644
--- a/packet_handler_map.go
+++ b/packet_handler_map.go
@@ -220,23 +220,6 @@ func (h *packetHandlerMap) GetByResetToken(token protocol.StatelessResetToken) (
 	return handler, ok
 }
 
-func (h *packetHandlerMap) CloseServer() {
-	h.mutex.Lock()
-	var wg sync.WaitGroup
-	for _, handler := range h.handlers {
-		if handler.getPerspective() == protocol.PerspectiveServer {
-			wg.Add(1)
-			go func(handler packetHandler) {
-				// blocks until the CONNECTION_CLOSE has been sent and the run-loop has stopped
-				handler.shutdown()
-				wg.Done()
-			}(handler)
-		}
-	}
-	h.mutex.Unlock()
-	wg.Wait()
-}
-
 func (h *packetHandlerMap) Close(e error) {
 	h.mutex.Lock()
 
diff --git a/server.go b/server.go
index 8353cdeb..f7c638b9 100644
--- a/server.go
+++ b/server.go
@@ -34,7 +34,6 @@ type packetHandlerManager interface {
 	GetByResetToken(protocol.StatelessResetToken) (packetHandler, bool)
 	AddWithConnID(protocol.ConnectionID, protocol.ConnectionID, func() (packetHandler, bool)) bool
 	Close(error)
-	CloseServer()
 	connRunner
 }
 
@@ -61,8 +60,6 @@ type rejectedPacket struct {
 
 // A Listener of QUIC
 type baseServer struct {
-	mutex sync.Mutex
-
 	disableVersionNegotiation bool
 	acceptEarlyConns          bool
 
@@ -104,10 +101,11 @@ type baseServer struct {
 		protocol.VersionNumber,
 	) quicConn
 
-	serverError             error
-	errorChan               chan struct{}
-	closed                  bool
-	running                 chan struct{} // closed as soon as run() returns
+	closeOnce sync.Once
+	errorChan chan struct{} // is closed when the server is closed
+	closeErr  error
+	running   chan struct{} // closed as soon as run() returns
+
 	versionNegotiationQueue chan receivedPacket
 	invalidTokenQueue       chan rejectedPacket
 	connectionRefusedQueue  chan rejectedPacket
@@ -132,7 +130,10 @@ func (l *Listener) Accept(ctx context.Context) (Connection, error) {
 	return l.baseServer.Accept(ctx)
 }
 
-// Close the server. All active connections will be closed.
+// Close closes the listener.
+// Accept will return ErrServerClosed as soon as all connections in the accept queue have been accepted.
+// QUIC handshakes that are still in flight will be rejected with a CONNECTION_REFUSED error.
+// Closing the listener doesn't have any effect on already established connections.
 func (l *Listener) Close() error {
 	return l.baseServer.Close()
 }
@@ -321,38 +322,25 @@ func (s *baseServer) accept(ctx context.Context) (quicConn, error) {
 		atomic.AddInt32(&s.connQueueLen, -1)
 		return conn, nil
 	case <-s.errorChan:
-		return nil, s.serverError
+		return nil, s.closeErr
 	}
 }
 
-// Close the server
 func (s *baseServer) Close() error {
-	s.mutex.Lock()
-	if s.closed {
-		s.mutex.Unlock()
-		return nil
-	}
-	if s.serverError == nil {
-		s.serverError = ErrServerClosed
-	}
-	s.closed = true
-	close(s.errorChan)
-	s.mutex.Unlock()
-
-	<-s.running
-	s.onClose()
+	s.close(ErrServerClosed, true)
 	return nil
 }
 
-func (s *baseServer) setCloseError(e error) {
-	s.mutex.Lock()
-	defer s.mutex.Unlock()
-	if s.closed {
-		return
-	}
-	s.closed = true
-	s.serverError = e
-	close(s.errorChan)
+func (s *baseServer) close(e error, notifyOnClose bool) {
+	s.closeOnce.Do(func() {
+		s.closeErr = e
+		close(s.errorChan)
+
+		<-s.running
+		if notifyOnClose {
+			s.onClose()
+		}
+	})
 }
 
 // Addr returns the server's network address
@@ -701,8 +689,11 @@ func (s *baseServer) handleInitialImpl(p receivedPacket, hdr *wire.Header) error
 func (s *baseServer) handleNewConn(conn quicConn) {
 	connCtx := conn.Context()
 	if s.acceptEarlyConns {
-		// wait until the early connection is ready (or the handshake fails)
+		// wait until the early connection is ready, the handshake fails, or the server is closed
 		select {
+		case <-s.errorChan:
+			conn.destroy(&qerr.TransportError{ErrorCode: ConnectionRefused})
+			return
 		case <-conn.earlyConnReady():
 		case <-connCtx.Done():
 			return
@@ -710,6 +701,9 @@ func (s *baseServer) handleNewConn(conn quicConn) {
 	} else {
 		// wait until the handshake is complete (or fails)
 		select {
+		case <-s.errorChan:
+			conn.destroy(&qerr.TransportError{ErrorCode: ConnectionRefused})
+			return
 		case <-conn.HandshakeComplete():
 		case <-connCtx.Done():
 			return
diff --git a/sys_conn_helper_linux.go b/sys_conn_helper_linux.go
index 8e326af9..5fbf34ad 100644
--- a/sys_conn_helper_linux.go
+++ b/sys_conn_helper_linux.go
@@ -19,7 +19,7 @@ const (
 	ipv4PKTINFO  = unix.IP_PKTINFO
 )
 
-const ecnIPv4DataLen = 4
+const ecnIPv4DataLen = 1
 
 const batchSize = 8 // needs to smaller than MaxUint8 (otherwise the type of oobConn.readPos has to be changed)
 
diff --git a/transport.go b/transport.go
index 60d44a43..38d35238 100644
--- a/transport.go
+++ b/transport.go
@@ -275,7 +275,8 @@ func (t *Transport) runSendQueue() {
 	}
 }
 
-// Close closes the underlying connection and waits until listen has returned.
+// Close closes the underlying connection.
+// If any listener was started, it will be closed as well.
 // It is invalid to start new listeners or connections after that.
 func (t *Transport) Close() error {
 	t.close(errors.New("closing"))
@@ -294,7 +295,6 @@ func (t *Transport) Close() error {
 }
 
 func (t *Transport) closeServer() {
-	t.handlerMap.CloseServer()
 	t.mutex.Lock()
 	t.server = nil
 	if t.isSingleUse {
@@ -322,7 +322,7 @@ func (t *Transport) close(e error) {
 		t.handlerMap.Close(e)
 	}
 	if t.server != nil {
-		t.server.setCloseError(e)
+		t.server.close(e, false)
 	}
 	t.closed = true
 }


Test patch:
diff --git a/integrationtests/gomodvendor/go.sum b/integrationtests/gomodvendor/go.sum
index 29427a8c..d65b491a 100644
--- a/integrationtests/gomodvendor/go.sum
+++ b/integrationtests/gomodvendor/go.sum
@@ -134,8 +134,8 @@ github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7q
 github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
 github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
-github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg=
-github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
+github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
+github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
diff --git a/integrationtests/self/cancelation_test.go b/integrationtests/self/cancelation_test.go
index 5f95c0b7..28f3e009 100644
--- a/integrationtests/self/cancelation_test.go
+++ b/integrationtests/self/cancelation_test.go
@@ -31,7 +31,7 @@ var _ = Describe("Stream Cancellations", func() {
 			server, err = quic.ListenAddr("localhost:0", getTLSConfig(), getQuicConfig(nil))
 			Expect(err).ToNot(HaveOccurred())
 
-			var canceledCounter int32
+			var canceledCounter atomic.Int32
 			go func() {
 				defer GinkgoRecover()
 				var wg sync.WaitGroup
@@ -50,18 +50,18 @@ var _ = Describe("Stream Cancellations", func() {
 								ErrorCode: quic.StreamErrorCode(str.StreamID()),
 								Remote:    true,
 							}))
-							atomic.AddInt32(&canceledCounter, 1)
+							canceledCounter.Add(1)
 							return
 						}
 						if err := str.Close(); err != nil {
 							Expect(err).To(MatchError(fmt.Sprintf("close called for canceled stream %d", str.StreamID())))
-							atomic.AddInt32(&canceledCounter, 1)
+							canceledCounter.Add(1)
 							return
 						}
 					}()
 				}
 				wg.Wait()
-				numCanceledStreamsChan <- atomic.LoadInt32(&canceledCounter)
+				numCanceledStreamsChan <- canceledCounter.Load()
 			}()
 			return numCanceledStreamsChan
 		}
@@ -80,7 +80,7 @@ var _ = Describe("Stream Cancellations", func() {
 			)
 			Expect(err).ToNot(HaveOccurred())
 
-			var canceledCounter int32
+			var canceledCounter atomic.Int32
 			var wg sync.WaitGroup
 			wg.Add(numStreams)
 			for i := 0; i < numStreams; i++ {
@@ -91,7 +91,7 @@ var _ = Describe("Stream Cancellations", func() {
 					Expect(err).ToNot(HaveOccurred())
 					// cancel around 2/3 of the streams
 					if rand.Int31()%3 != 0 {
-						atomic.AddInt32(&canceledCounter, 1)
+						canceledCounter.Add(1)
 						resetErr := quic.StreamErrorCode(str.StreamID())
 						str.CancelRead(resetErr)
 						_, err := str.Read([]byte{0})
@@ -113,7 +113,7 @@ var _ = Describe("Stream Cancellations", func() {
 			Eventually(serverCanceledCounterChan).Should(Receive(&serverCanceledCounter))
 			Expect(conn.CloseWithError(0, "")).To(Succeed())
 
-			clientCanceledCounter := atomic.LoadInt32(&canceledCounter)
+			clientCanceledCounter := canceledCounter.Load()
 			// The server will only count a stream as being reset if learns about the cancelation before it finished writing all data.
 			Expect(clientCanceledCounter).To(BeNumerically(">=", serverCanceledCounter))
 			fmt.Fprintf(GinkgoWriter, "Canceled reading on %d of %d streams.\n", clientCanceledCounter, numStreams)
@@ -132,7 +132,7 @@ var _ = Describe("Stream Cancellations", func() {
 			)
 			Expect(err).ToNot(HaveOccurred())
 
-			var canceledCounter int32
+			var canceledCounter atomic.Int32
 			var wg sync.WaitGroup
 			wg.Add(numStreams)
 			for i := 0; i < numStreams; i++ {
@@ -148,7 +148,7 @@ var _ = Describe("Stream Cancellations", func() {
 						Expect(err).ToNot(HaveOccurred())
 						str.CancelRead(quic.StreamErrorCode(str.StreamID()))
 						Expect(data).To(Equal(PRData[:length]))
-						atomic.AddInt32(&canceledCounter, 1)
+						canceledCounter.Add(1)
 						return
 					}
 					data, err := io.ReadAll(str)
@@ -162,7 +162,7 @@ var _ = Describe("Stream Cancellations", func() {
 			Eventually(serverCanceledCounterChan).Should(Receive(&serverCanceledCounter))
 			Expect(conn.CloseWithError(0, "")).To(Succeed())
 
-			clientCanceledCounter := atomic.LoadInt32(&canceledCounter)
+			clientCanceledCounter := canceledCounter.Load()
 			// The server will only count a stream as being reset if learns about the cancelation before it finished writing all data.
 			Expect(clientCanceledCounter).To(BeNumerically(">=", serverCanceledCounter))
 			fmt.Fprintf(GinkgoWriter, "Canceled reading on %d of %d streams.\n", clientCanceledCounter, numStreams)
@@ -185,7 +185,7 @@ var _ = Describe("Stream Cancellations", func() {
 
 			var wg sync.WaitGroup
 			wg.Add(numStreams)
-			var counter int32
+			var counter atomic.Int32
 			for i := 0; i < numStreams; i++ {
 				go func() {
 					defer GinkgoRecover()
@@ -199,7 +199,7 @@ var _ = Describe("Stream Cancellations", func() {
 						defer close(done)
 						b := make([]byte, 32)
 						if _, err := str.Read(b); err != nil {
-							atomic.AddInt32(&counter, 1)
+							counter.Add(1)
 							Expect(err).To(Equal(&quic.StreamError{
 								StreamID:  str.StreamID(),
 								ErrorCode: 1234,
@@ -214,7 +214,7 @@ var _ = Describe("Stream Cancellations", func() {
 			}
 			wg.Wait()
 			Expect(conn.CloseWithError(0, "")).To(Succeed())
-			numCanceled := atomic.LoadInt32(&counter)
+			numCanceled := counter.Load()
 			fmt.Fprintf(GinkgoWriter, "canceled %d out of %d streams", numCanceled, numStreams)
 			Expect(numCanceled).ToNot(BeZero())
 			Eventually(serverCanceledCounterChan).Should(Receive())
@@ -232,7 +232,7 @@ var _ = Describe("Stream Cancellations", func() {
 			Expect(err).ToNot(HaveOccurred())
 
 			var wg sync.WaitGroup
-			var counter int32
+			var counter atomic.Int32
 			wg.Add(numStreams)
 			for i := 0; i < numStreams; i++ {
 				go func() {
@@ -242,7 +242,7 @@ var _ = Describe("Stream Cancellations", func() {
 					Expect(err).ToNot(HaveOccurred())
 					data, err := io.ReadAll(str)
 					if err != nil {
-						atomic.AddInt32(&counter, 1)
+						counter.Add(1)
 						Expect(err).To(MatchError(&quic.StreamError{
 							StreamID:  str.StreamID(),
 							ErrorCode: quic.StreamErrorCode(str.StreamID()),
@@ -254,7 +254,7 @@ var _ = Describe("Stream Cancellations", func() {
 			}
 			wg.Wait()
 
-			streamCount := atomic.LoadInt32(&counter)
+			streamCount := counter.Load()
 			fmt.Fprintf(GinkgoWriter, "Canceled writing on %d of %d streams\n", streamCount, numStreams)
 			Expect(streamCount).To(BeNumerically(">", numStreams/10))
 			Expect(numStreams - streamCount).To(BeNumerically(">", numStreams/10))
@@ -267,7 +267,7 @@ var _ = Describe("Stream Cancellations", func() {
 			server, err := quic.ListenAddr("localhost:0", getTLSConfig(), nil)
 			Expect(err).ToNot(HaveOccurred())
 
-			var canceledCounter int32
+			var canceledCounter atomic.Int32
 			go func() {
 				defer GinkgoRecover()
 				conn, err := server.Accept(context.Background())
@@ -280,7 +280,7 @@ var _ = Describe("Stream Cancellations", func() {
 						// cancel about 2/3 of the streams
 						if rand.Int31()%3 != 0 {
 							str.CancelWrite(quic.StreamErrorCode(str.StreamID()))
-							atomic.AddInt32(&canceledCounter, 1)
+							canceledCounter.Add(1)
 							return
 						}
 						_, err = str.Write(PRData)
@@ -291,14 +291,14 @@ var _ = Describe("Stream Cancellations", func() {
 			}()
 
 			clientCanceledStreams := runClient(server)
-			Expect(clientCanceledStreams).To(Equal(atomic.LoadInt32(&canceledCounter)))
+			Expect(clientCanceledStreams).To(Equal(canceledCounter.Load()))
 		})
 
 		It("downloads when the server cancels some streams after sending some data", func() {
 			server, err := quic.ListenAddr("localhost:0", getTLSConfig(), nil)
 			Expect(err).ToNot(HaveOccurred())
 
-			var canceledCounter int32
+			var canceledCounter atomic.Int32
 			go func() {
 				defer GinkgoRecover()
 				conn, err := server.Accept(context.Background())
@@ -314,7 +314,7 @@ var _ = Describe("Stream Cancellations", func() {
 							_, err = str.Write(PRData[:length])
 							Expect(err).ToNot(HaveOccurred())
 							str.CancelWrite(quic.StreamErrorCode(str.StreamID()))
-							atomic.AddInt32(&canceledCounter, 1)
+							canceledCounter.Add(1)
 							return
 						}
 						_, err = str.Write(PRData)
@@ -325,7 +325,7 @@ var _ = Describe("Stream Cancellations", func() {
 			}()
 
 			clientCanceledStreams := runClient(server)
-			Expect(clientCanceledStreams).To(Equal(atomic.LoadInt32(&canceledCounter)))
+			Expect(clientCanceledStreams).To(Equal(canceledCounter.Load()))
 		})
 	})
 
@@ -378,7 +378,7 @@ var _ = Describe("Stream Cancellations", func() {
 			Expect(err).ToNot(HaveOccurred())
 
 			var wg sync.WaitGroup
-			var counter int32
+			var counter atomic.Int32
 			wg.Add(numStreams)
 			for i := 0; i < numStreams; i++ {
 				go func() {
@@ -399,13 +399,13 @@ var _ = Describe("Stream Cancellations", func() {
 						}))
 						return
 					}
-					atomic.AddInt32(&counter, 1)
+					counter.Add(1)
 					Expect(data).To(Equal(PRData))
 				}()
 			}
 			wg.Wait()
 
-			count := atomic.LoadInt32(&counter)
+			count := counter.Load()
 			Expect(count).To(BeNumerically(">", numStreams/15))
 			fmt.Fprintf(GinkgoWriter, "Successfully read from %d of %d streams.\n", count, numStreams)
 
@@ -464,7 +464,7 @@ var _ = Describe("Stream Cancellations", func() {
 			Expect(err).ToNot(HaveOccurred())
 
 			var wg sync.WaitGroup
-			var counter int32
+			var counter atomic.Int32
 			wg.Add(numStreams)
 			for i := 0; i < numStreams; i++ {
 				go func() {
@@ -495,14 +495,14 @@ var _ = Describe("Stream Cancellations", func() {
 						return
 					}
 
-					atomic.AddInt32(&counter, 1)
+					counter.Add(1)
 					Expect(data).To(Equal(PRData))
 				}()
 			}
 			wg.Wait()
 			Eventually(done).Should(BeClosed())
 
-			count := atomic.LoadInt32(&counter)
+			count := counter.Load()
 			Expect(count).To(BeNumerically(">", numStreams/15))
 			fmt.Fprintf(GinkgoWriter, "Successfully read from %d of %d streams.\n", count, numStreams)
 
@@ -543,7 +543,7 @@ var _ = Describe("Stream Cancellations", func() {
 			Expect(err).ToNot(HaveOccurred())
 
 			var numToAccept int
-			var counter int32
+			var counter atomic.Int32
 			var wg sync.WaitGroup
 			wg.Add(numStreams)
 			for numToAccept < numStreams {
@@ -561,7 +561,7 @@ var _ = Describe("Stream Cancellations", func() {
 					str, err := conn.AcceptUniStream(ctx)
 					if err != nil {
 						if err.Error() == "context canceled" {
-							atomic.AddInt32(&counter, 1)
+							counter.Add(1)
 						}
 						return
 					}
@@ -573,7 +573,7 @@ var _ = Describe("Stream Cancellations", func() {
 			}
 			wg.Wait()
 
-			count := atomic.LoadInt32(&counter)
+			count := counter.Load()
 			fmt.Fprintf(GinkgoWriter, "Canceled AcceptStream %d times\n", count)
 			Expect(count).To(BeNumerically(">", numStreams/2))
 			Expect(conn.CloseWithError(0, "")).To(Succeed())
@@ -589,7 +589,7 @@ var _ = Describe("Stream Cancellations", func() {
 			Expect(err).ToNot(HaveOccurred())
 
 			msg := make(chan struct{}, 1)
-			var numCanceled int32
+			var numCanceled atomic.Int32
 			go func() {
 				defer GinkgoRecover()
 				defer close(msg)
@@ -603,7 +603,7 @@ var _ = Describe("Stream Cancellations", func() {
 					str, err := conn.OpenUniStreamSync(ctx)
 					if err != nil {
 						Expect(err).To(MatchError(context.DeadlineExceeded))
-						atomic.AddInt32(&numCanceled, 1)
+						numCanceled.Add(1)
 						select {
 						case msg <- struct{}{}:
 						default:
@@ -644,7 +644,7 @@ var _ = Describe("Stream Cancellations", func() {
 			}
 			wg.Wait()
 
-			count := atomic.LoadInt32(&numCanceled)
+			count := numCanceled.Load()
 			fmt.Fprintf(GinkgoWriter, "Canceled OpenStreamSync %d times\n", count)
 			Expect(count).To(BeNumerically(">=", numStreams-maxIncomingStreams))
 			Expect(conn.CloseWithError(0, "")).To(Succeed())
diff --git a/integrationtests/self/datagram_test.go b/integrationtests/self/datagram_test.go
index 35d0718a..55e85771 100644
--- a/integrationtests/self/datagram_test.go
+++ b/integrationtests/self/datagram_test.go
@@ -23,7 +23,7 @@ var _ = Describe("Datagram test", func() {
 
 	var (
 		serverConn, clientConn *net.UDPConn
-		dropped, total         int32
+		dropped, total         atomic.Int32
 	)
 
 	startServerAndProxy := func(enableDatagram, expectDatagramSupport bool) (port int, closeFn func()) {
@@ -57,7 +57,7 @@ var _ = Describe("Datagram test", func() {
 							defer wg.Done()
 							b := make([]byte, 8)
 							binary.BigEndian.PutUint64(b, uint64(i))
-							Expect(conn.SendMessage(b)).To(Succeed())
+							Expect(conn.SendDatagram(b)).To(Succeed())
 						}(i)
 					}
 					wg.Wait()
@@ -81,9 +81,9 @@ var _ = Describe("Datagram test", func() {
 				}
 				drop := mrand.Int()%10 == 0
 				if drop {
-					atomic.AddInt32(&dropped, 1)
+					dropped.Add(1)
 				}
-				atomic.AddInt32(&total, 1)
+				total.Add(1)
 				return drop
 			},
 		})
@@ -120,16 +120,16 @@ var _ = Describe("Datagram test", func() {
 		for {
 			// Close the connection if no message is received for 100 ms.
 			timer := time.AfterFunc(scaleDuration(100*time.Millisecond), func() { conn.CloseWithError(0, "") })
-			if _, err := conn.ReceiveMessage(context.Background()); err != nil {
+			if _, err := conn.ReceiveDatagram(context.Background()); err != nil {
 				break
 			}
 			timer.Stop()
 			counter++
 		}
 
-		numDropped := int(atomic.LoadInt32(&dropped))
+		numDropped := int(dropped.Load())
 		expVal := num - numDropped
-		fmt.Fprintf(GinkgoWriter, "Dropped %d out of %d packets.\n", numDropped, atomic.LoadInt32(&total))
+		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)
 		Expect(counter).To(And(
 			BeNumerically(">", expVal*9/10),
@@ -170,7 +170,7 @@ var _ = Describe("Datagram test", func() {
 		Expect(err).ToNot(HaveOccurred())
 		Expect(conn.ConnectionState().SupportsDatagrams).To(BeFalse())
 
-		Expect(conn.SendMessage([]byte{0})).To(HaveOccurred())
+		Expect(conn.SendDatagram([]byte{0})).To(HaveOccurred())
 
 		close()
 		conn.CloseWithError(0, "")
diff --git a/integrationtests/self/deadline_test.go b/integrationtests/self/deadline_test.go
index b165aff0..43e3b247 100644
--- a/integrationtests/self/deadline_test.go
+++ b/integrationtests/self/deadline_test.go
@@ -14,7 +14,7 @@ import (
 )
 
 var _ = Describe("Stream deadline tests", func() {
-	setup := func() (*quic.Listener, quic.Stream, quic.Stream) {
+	setup := func() (serverStr, clientStr quic.Stream, close func()) {
 		server, err := quic.ListenAddr("localhost:0", getTLSConfig(), getQuicConfig(nil))
 		Expect(err).ToNot(HaveOccurred())
 		strChan := make(chan quic.SendStream)
@@ -36,19 +36,21 @@ var _ = Describe("Stream deadline tests", func() {
 			getQuicConfig(nil),
 		)
 		Expect(err).ToNot(HaveOccurred())
-		clientStr, err := conn.OpenStream()
+		clientStr, err = conn.OpenStream()
 		Expect(err).ToNot(HaveOccurred())
 		_, err = clientStr.Write([]byte{0}) // need to write one byte so the server learns about the stream
 		Expect(err).ToNot(HaveOccurred())
-		var serverStr quic.Stream
 		Eventually(strChan).Should(Receive(&serverStr))
-		return server, serverStr, clientStr
+		return serverStr, clientStr, func() {
+			Expect(server.Close()).To(Succeed())
+			Expect(conn.CloseWithError(0, "")).To(Succeed())
+		}
 	}
 
 	Context("read deadlines", func() {
 		It("completes a transfer when the deadline is set", func() {
-			server, serverStr, clientStr := setup()
-			defer server.Close()
+			serverStr, clientStr, closeFn := setup()
+			defer closeFn()
 
 			const timeout = time.Millisecond
 			done := make(chan struct{})
@@ -82,8 +84,8 @@ var _ = Describe("Stream deadline tests", func() {
 		})
 
 		It("completes a transfer when the deadline is set concurrently", func() {
-			server, serverStr, clientStr := setup()
-			defer server.Close()
+			serverStr, clientStr, closeFn := setup()
+			defer closeFn()
 
 			const timeout = time.Millisecond
 			go func() {
@@ -132,8 +134,8 @@ var _ = Describe("Stream deadline tests", func() {
 
 	Context("write deadlines", func() {
 		It("completes a transfer when the deadline is set", func() {
-			server, serverStr, clientStr := setup()
-			defer server.Close()
+			serverStr, clientStr, closeFn := setup()
+			defer closeFn()
 
 			const timeout = time.Millisecond
 			done := make(chan struct{})
@@ -165,8 +167,8 @@ var _ = Describe("Stream deadline tests", func() {
 		})
 
 		It("completes a transfer when the deadline is set concurrently", func() {
-			server, serverStr, clientStr := setup()
-			defer server.Close()
+			serverStr, clientStr, closeFn := setup()
+			defer closeFn()
 
 			const timeout = time.Millisecond
 			readDone := make(chan struct{})
diff --git a/integrationtests/self/drop_test.go b/integrationtests/self/drop_test.go
index 4eac657a..a3e385bb 100644
--- a/integrationtests/self/drop_test.go
+++ b/integrationtests/self/drop_test.go
@@ -67,14 +67,14 @@ var _ = Describe("Drop Tests", func() {
 				fmt.Fprintf(GinkgoWriter, "Dropping packets for %s, after a delay of %s\n", dropDuration, dropDelay)
 				startTime := time.Now()
 
-				var numDroppedPackets int32
+				var numDroppedPackets atomic.Int32
 				startListenerAndProxy(func(d quicproxy.Direction, _ []byte) bool {
 					if !d.Is(direction) {
 						return false
 					}
 					drop := time.Now().After(startTime.Add(dropDelay)) && time.Now().Before(startTime.Add(dropDelay).Add(dropDuration))
 					if drop {
-						atomic.AddInt32(&numDroppedPackets, 1)
+						numDroppedPackets.Add(1)
 					}
 					return drop
 				})
@@ -114,7 +114,7 @@ var _ = Describe("Drop Tests", func() {
 					Expect(b[0]).To(Equal(i))
 				}
 				close(done)
-				numDropped := atomic.LoadInt32(&numDroppedPackets)
+				numDropped := numDroppedPackets.Load()
 				fmt.Fprintf(GinkgoWriter, "Dropped %d packets.\n", numDropped)
 				Expect(numDropped).To(BeNumerically(">", 0))
 			})
diff --git a/integrationtests/self/handshake_drop_test.go b/integrationtests/self/handshake_drop_test.go
index ae483771..a0f23479 100644
--- a/integrationtests/self/handshake_drop_test.go
+++ b/integrationtests/self/handshake_drop_test.go
@@ -194,15 +194,15 @@ var _ = Describe("Handshake drop tests", func() {
 
 							Context(app.name, func() {
 								It(fmt.Sprintf("establishes a connection when the first packet is lost in %s direction", direction), func() {
-									var incoming, outgoing int32
+									var incoming, outgoing atomic.Int32
 									startListenerAndProxy(func(d quicproxy.Direction, _ []byte) bool {
 										var p int32
 										//nolint:exhaustive
 										switch d {
 										case quicproxy.DirectionIncoming:
-											p = atomic.AddInt32(&incoming, 1)
+											p = incoming.Add(1)
 										case quicproxy.DirectionOutgoing:
-											p = atomic.AddInt32(&outgoing, 1)
+											p = outgoing.Add(1)
 										}
 										return p == 1 && d.Is(direction)
 									}, doRetry, longCertChain)
@@ -210,15 +210,15 @@ var _ = Describe("Handshake drop tests", func() {
 								})
 
 								It(fmt.Sprintf("establishes a connection when the second packet is lost in %s direction", direction), func() {
-									var incoming, outgoing int32
+									var incoming, outgoing atomic.Int32
 									startListenerAndProxy(func(d quicproxy.Direction, _ []byte) bool {
 										var p int32
 										//nolint:exhaustive
 										switch d {
 										case quicproxy.DirectionIncoming:
-											p = atomic.AddInt32(&incoming, 1)
+											p = incoming.Add(1)
 										case quicproxy.DirectionOutgoing:
-											p = atomic.AddInt32(&outgoing, 1)
+											p = outgoing.Add(1)
 										}
 										return p == 2 && d.Is(direction)
 									}, doRetry, longCertChain)
diff --git a/integrationtests/self/handshake_test.go b/integrationtests/self/handshake_test.go
index d4f65a12..141487ab 100644
--- a/integrationtests/self/handshake_test.go
+++ b/integrationtests/self/handshake_test.go
@@ -152,13 +152,14 @@ var _ = Describe("Handshake tests", func() {
 	Context("Certificate validation", func() {
 		It("accepts the certificate", func() {
 			runServer(getTLSConfig())
-			_, err := quic.DialAddr(
+			conn, err := quic.DialAddr(
 				context.Background(),
 				fmt.Sprintf("localhost:%d", server.Addr().(*net.UDPAddr).Port),
 				getTLSClientConfig(),
 				getQuicConfig(nil),
 			)
 			Expect(err).ToNot(HaveOccurred())
+			conn.CloseWithError(0, "")
 		})
 
 		It("has the right local and remote address on the tls.Config.GetConfigForClient ClientHelloInfo.Conn", func() {
@@ -187,6 +188,7 @@ var _ = Describe("Handshake tests", func() {
 				getQuicConfig(nil),
 			)
 			Expect(err).ToNot(HaveOccurred())
+			defer conn.CloseWithError(0, "")
 			Eventually(done).Should(BeClosed())
 			Expect(server.Addr()).To(Equal(local))
 			Expect(conn.LocalAddr().(*net.UDPAddr).Port).To(Equal(remote.(*net.UDPAddr).Port))
@@ -196,13 +198,14 @@ var _ = Describe("Handshake tests", func() {
 
 		It("works with a long certificate chain", func() {
 			runServer(getTLSConfigWithLongCertChain())
-			_, err := quic.DialAddr(
+			conn, err := quic.DialAddr(
 				context.Background(),
 				fmt.Sprintf("localhost:%d", server.Addr().(*net.UDPAddr).Port),
 				getTLSClientConfig(),
 				getQuicConfig(nil),
 			)
 			Expect(err).ToNot(HaveOccurred())
+			conn.CloseWithError(0, "")
 		})
 
 		It("errors if the server name doesn't match", func() {
diff --git a/integrationtests/self/hotswap_test.go b/integrationtests/self/hotswap_test.go
index ac75a494..232a13c3 100644
--- a/integrationtests/self/hotswap_test.go
+++ b/integrationtests/self/hotswap_test.go
@@ -20,7 +20,7 @@ import (
 type listenerWrapper struct {
 	http3.QUICEarlyListener
 	listenerClosed bool
-	count          int32
+	count          atomic.Int32
 }
 
 func (ln *listenerWrapper) Close() error {
@@ -29,14 +29,18 @@ func (ln *listenerWrapper) Close() error {
 }
 
 func (ln *listenerWrapper) Faker() *fakeClosingListener {
-	atomic.AddInt32(&ln.count, 1)
+	ln.count.Add(1)
 	ctx, cancel := context.WithCancel(context.Background())
-	return &fakeClosingListener{ln, 0, ctx, cancel}
+	return &fakeClosingListener{
+		listenerWrapper: ln,
+		ctx:             ctx,
+		cancel:          cancel,
+	}
 }
 
 type fakeClosingListener struct {
 	*listenerWrapper
-	closed int32
+	closed atomic.Bool
 	ctx    context.Context
 	cancel context.CancelFunc
 }
@@ -47,9 +51,9 @@ func (ln *fakeClosingListener) Accept(ctx context.Context) (quic.EarlyConnection
 }
 
 func (ln *fakeClosingListener) Close() error {
-	if atomic.CompareAndSwapInt32(&ln.closed, 0, 1) {
+	if ln.closed.CompareAndSwap(false, true) {
 		ln.cancel()
-		if atomic.AddInt32(&ln.listenerWrapper.count, -1) == 0 {
+		if ln.listenerWrapper.count.Add(-1) == 0 {
 			ln.listenerWrapper.Close()
 		}
 	}
@@ -145,8 +149,8 @@ var _ = Describe("HTTP3 Server hotswap test", func() {
 		// and only the fake listener should be closed
 		Expect(server1.Close()).NotTo(HaveOccurred())
 		Eventually(stoppedServing1).Should(BeClosed())
-		Expect(fake1.closed).To(Equal(int32(1)))
-		Expect(fake2.closed).To(Equal(int32(0)))
+		Expect(fake1.closed.Load()).To(BeTrue())
+		Expect(fake2.closed.Load()).To(BeFalse())
 		Expect(ln.listenerClosed).ToNot(BeTrue())
 		Expect(client.Transport.(*http3.RoundTripper).Close()).NotTo(HaveOccurred())
 
@@ -161,7 +165,7 @@ var _ = Describe("HTTP3 Server hotswap test", func() {
 		// close the other server - both the fake and the actual listeners must close now
 		Expect(server2.Close()).NotTo(HaveOccurred())
 		Eventually(stoppedServing2).Should(BeClosed())
-		Expect(fake2.closed).To(Equal(int32(1)))
+		Expect(fake2.closed.Load()).To(BeTrue())
 		Expect(ln.listenerClosed).To(BeTrue())
 	})
 })
diff --git a/integrationtests/self/mitm_test.go b/integrationtests/self/mitm_test.go
index a5f07f77..0eb68d06 100644
--- a/integrationtests/self/mitm_test.go
+++ b/integrationtests/self/mitm_test.go
@@ -244,17 +244,17 @@ var _ = Describe("MITM test", func() {
 		Context("corrupting packets", func() {
 			const idleTimeout = time.Second
 
-			var numCorrupted, numPackets int32
+			var numCorrupted, numPackets atomic.Int32
 
 			BeforeEach(func() {
-				numCorrupted = 0
-				numPackets = 0
+				numCorrupted.Store(0)
+				numPackets.Store(0)
 				serverConfig.MaxIdleTimeout = idleTimeout
 			})
 
 			AfterEach(func() {
-				num := atomic.LoadInt32(&numCorrupted)
-				fmt.Fprintf(GinkgoWriter, "Corrupted %d of %d packets.", num, atomic.LoadInt32(&numPackets))
+				num := numCorrupted.Load()
+				fmt.Fprintf(GinkgoWriter, "Corrupted %d of %d packets.", num, numPackets.Load())
 				Expect(num).To(BeNumerically(">=", 1))
 				// If the packet containing the CONNECTION_CLOSE is corrupted,
 				// we have to wait for the connection to time out.
@@ -266,13 +266,13 @@ var _ = Describe("MITM test", func() {
 				dropCb := func(dir quicproxy.Direction, raw []byte) bool {
 					defer GinkgoRecover()
 					if dir == quicproxy.DirectionIncoming {
-						atomic.AddInt32(&numPackets, 1)
+						numPackets.Add(1)
 						if rand.Intn(interval) == 0 {
 							pos := rand.Intn(len(raw))
 							raw[pos] = byte(rand.Intn(256))
 							_, err := clientTransport.WriteTo(raw, serverTransport.Conn.LocalAddr())
 							Expect(err).ToNot(HaveOccurred())
-							atomic.AddInt32(&numCorrupted, 1)
+							numCorrupted.Add(1)
 							return true
 						}
 					}
@@ -286,13 +286,13 @@ var _ = Describe("MITM test", func() {
 				dropCb := func(dir quicproxy.Direction, raw []byte) bool {
 					defer GinkgoRecover()
 					if dir == quicproxy.DirectionOutgoing {
-						atomic.AddInt32(&numPackets, 1)
+						numPackets.Add(1)
 						if rand.Intn(interval) == 0 {
 							pos := rand.Intn(len(raw))
 							raw[pos] = byte(rand.Intn(256))
 							_, err := serverTransport.WriteTo(raw, clientTransport.Conn.LocalAddr())
 							Expect(err).ToNot(HaveOccurred())
-							atomic.AddInt32(&numCorrupted, 1)
+							numCorrupted.Add(1)
 							return true
 						}
 					}
diff --git a/integrationtests/self/resumption_test.go b/integrationtests/self/resumption_test.go
index bedf0470..23f241be 100644
--- a/integrationtests/self/resumption_test.go
+++ b/integrationtests/self/resumption_test.go
@@ -52,22 +52,23 @@ var _ = Describe("TLS session resumption", func() {
 		cache := newClientSessionCache(tls.NewLRUClientSessionCache(10), gets, puts)
 		tlsConf := getTLSClientConfig()
 		tlsConf.ClientSessionCache = cache
-		conn, err := quic.DialAddr(
+		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(conn.ConnectionState().TLS.DidResume).To(BeFalse())
+		Expect(conn1.ConnectionState().TLS.DidResume).To(BeFalse())
 
 		serverConn, err := server.Accept(context.Background())
 		Expect(err).ToNot(HaveOccurred())
 		Expect(serverConn.ConnectionState().TLS.DidResume).To(BeFalse())
 
-		conn, err = quic.DialAddr(
+		conn2, err := quic.DialAddr(
 			context.Background(),
 			fmt.Sprintf("localhost:%d", server.Addr().(*net.UDPAddr).Port),
 			tlsConf,
@@ -75,11 +76,12 @@ var _ = Describe("TLS session resumption", func() {
 		)
 		Expect(err).ToNot(HaveOccurred())
 		Expect(gets).To(Receive(Equal(sessionKey)))
-		Expect(conn.ConnectionState().TLS.DidResume).To(BeTrue())
+		Expect(conn2.ConnectionState().TLS.DidResume).To(BeTrue())
 
 		serverConn, err = server.Accept(context.Background())
 		Expect(err).ToNot(HaveOccurred())
 		Expect(serverConn.ConnectionState().TLS.DidResume).To(BeTrue())
+		conn2.CloseWithError(0, "")
 	})
 
 	It("doesn't use session resumption, if the config disables it", func() {
@@ -94,15 +96,16 @@ var _ = Describe("TLS session resumption", func() {
 		cache := newClientSessionCache(tls.NewLRUClientSessionCache(10), gets, puts)
 		tlsConf := getTLSClientConfig()
 		tlsConf.ClientSessionCache = cache
-		conn, err := quic.DialAddr(
+		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, "")
 		Consistently(puts).ShouldNot(Receive())
-		Expect(conn.ConnectionState().TLS.DidResume).To(BeFalse())
+		Expect(conn1.ConnectionState().TLS.DidResume).To(BeFalse())
 
 		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
 		defer cancel()
@@ -110,14 +113,15 @@ var _ = Describe("TLS session resumption", func() {
 		Expect(err).ToNot(HaveOccurred())
 		Expect(serverConn.ConnectionState().TLS.DidResume).To(BeFalse())
 
-		conn, err = quic.DialAddr(
+		conn2, err := quic.DialAddr(
 			context.Background(),
 			fmt.Sprintf("localhost:%d", server.Addr().(*net.UDPAddr).Port),
 			tlsConf,
 			getQuicConfig(nil),
 		)
 		Expect(err).ToNot(HaveOccurred())
-		Expect(conn.ConnectionState().TLS.DidResume).To(BeFalse())
+		Expect(conn2.ConnectionState().TLS.DidResume).To(BeFalse())
+		defer conn2.CloseWithError(0, "")
 
 		serverConn, err = server.Accept(context.Background())
 		Expect(err).ToNot(HaveOccurred())
@@ -142,7 +146,7 @@ var _ = Describe("TLS session resumption", func() {
 		cache := newClientSessionCache(tls.NewLRUClientSessionCache(10), gets, puts)
 		tlsConf := getTLSClientConfig()
 		tlsConf.ClientSessionCache = cache
-		conn, err := quic.DialAddr(
+		conn1, err := quic.DialAddr(
 			context.Background(),
 			fmt.Sprintf("localhost:%d", server.Addr().(*net.UDPAddr).Port),
 			tlsConf,
@@ -150,7 +154,8 @@ var _ = Describe("TLS session resumption", func() {
 		)
 		Expect(err).ToNot(HaveOccurred())
 		Consistently(puts).ShouldNot(Receive())
-		Expect(conn.ConnectionState().TLS.DidResume).To(BeFalse())
+		Expect(conn1.ConnectionState().TLS.DidResume).To(BeFalse())
+		defer conn1.CloseWithError(0, "")
 
 		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
 		defer cancel()
@@ -158,14 +163,15 @@ var _ = Describe("TLS session resumption", func() {
 		Expect(err).ToNot(HaveOccurred())
 		Expect(serverConn.ConnectionState().TLS.DidResume).To(BeFalse())
 
-		conn, err = quic.DialAddr(
+		conn2, err := quic.DialAddr(
 			context.Background(),
 			fmt.Sprintf("localhost:%d", server.Addr().(*net.UDPAddr).Port),
 			tlsConf,
 			getQuicConfig(nil),
 		)
 		Expect(err).ToNot(HaveOccurred())
-		Expect(conn.ConnectionState().TLS.DidResume).To(BeFalse())
+		Expect(conn2.ConnectionState().TLS.DidResume).To(BeFalse())
+		defer conn2.CloseWithError(0, "")
 
 		serverConn, err = server.Accept(context.Background())
 		Expect(err).ToNot(HaveOccurred())
diff --git a/integrationtests/self/timeout_test.go b/integrationtests/self/timeout_test.go
index ddd0b973..da4c6883 100644
--- a/integrationtests/self/timeout_test.go
+++ b/integrationtests/self/timeout_test.go
@@ -22,12 +22,12 @@ type faultyConn struct {
 	net.PacketConn
 
 	MaxPackets int32
-	counter    int32
+	counter    atomic.Int32
 }
 
 func (c *faultyConn) ReadFrom(p []byte) (int, net.Addr, error) {
 	n, addr, err := c.PacketConn.ReadFrom(p)
-	counter := atomic.AddInt32(&c.counter, 1)
+	counter := c.counter.Add(1)
 	if counter <= c.MaxPackets {
 		return n, addr, err
 	}
@@ -35,7 +35,7 @@ func (c *faultyConn) ReadFrom(p []byte) (int, net.Addr, error) {
 }
 
 func (c *faultyConn) WriteTo(p []byte, addr net.Addr) (int, error) {
-	counter := atomic.AddInt32(&c.counter, 1)
+	counter := c.counter.Add(1)
 	if counter <= c.MaxPackets {
 		return c.PacketConn.WriteTo(p, addr)
 	}
@@ -185,11 +185,13 @@ var _ = Describe("Timeout tests", func() {
 			Expect(err).ToNot(HaveOccurred())
 			defer server.Close()
 
+			serverConnChan := make(chan quic.Connection, 1)
 			serverConnClosed := make(chan struct{})
 			go func() {
 				defer GinkgoRecover()
 				conn, err := server.Accept(context.Background())
 				Expect(err).ToNot(HaveOccurred())
+				serverConnChan <- conn
 				conn.AcceptStream(context.Background()) // blocks until the connection is closed
 				close(serverConnClosed)
 			}()
@@ -240,7 +242,7 @@ var _ = Describe("Timeout tests", func() {
 			Consistently(serverConnClosed).ShouldNot(BeClosed())
 
 			// make the go routine return
-			Expect(server.Close()).To(Succeed())
+			(<-serverConnChan).CloseWithError(0, "")
 			Eventually(serverConnClosed).Should(BeClosed())
 		})
 
@@ -266,11 +268,13 @@ var _ = Describe("Timeout tests", func() {
 			Expect(err).ToNot(HaveOccurred())
 			defer proxy.Close()
 
+			serverConnChan := make(chan quic.Connection, 1)
 			serverConnClosed := make(chan struct{})
 			go func() {
 				defer GinkgoRecover()
 				conn, err := server.Accept(context.Background())
 				Expect(err).ToNot(HaveOccurred())
+				serverConnChan <- conn
 				<-conn.Context().Done() // block until the connection is closed
 				close(serverConnClosed)
 			}()
@@ -309,7 +313,7 @@ var _ = Describe("Timeout tests", func() {
 			Consistently(serverConnClosed).ShouldNot(BeClosed())
 
 			// make the go routine return
-			Expect(server.Close()).To(Succeed())
+			(<-serverConnChan).CloseWithError(0, "")
 			Eventually(serverConnClosed).Should(BeClosed())
 		})
 	})
@@ -325,11 +329,13 @@ var _ = Describe("Timeout tests", func() {
 		Expect(err).ToNot(HaveOccurred())
 		defer server.Close()
 
+		serverConnChan := make(chan quic.Connection, 1)
 		serverConnClosed := make(chan struct{})
 		go func() {
 			defer GinkgoRecover()
 			conn, err := server.Accept(context.Background())
 			Expect(err).ToNot(HaveOccurred())
+			serverConnChan <- conn
 			conn.AcceptStream(context.Background()) // blocks until the connection is closed
 			close(serverConnClosed)
 		}()
@@ -370,7 +376,7 @@ var _ = Describe("Timeout tests", func() {
 		_, err = str.Write([]byte("foobar"))
 		checkTimeoutError(err)
 
-		Expect(server.Close()).To(Succeed())
+		(<-serverConnChan).CloseWithError(0, "")
 		Eventually(serverConnClosed).Should(BeClosed())
 	})
 
diff --git a/integrationtests/self/tracer_test.go b/integrationtests/self/tracer_test.go
index 5179646e..21691661 100644
--- a/integrationtests/self/tracer_test.go
+++ b/integrationtests/self/tracer_test.go
@@ -19,7 +19,7 @@ import (
 	. "github.com/onsi/gomega"
 )
 
-var _ = Describe("Handshake tests", func() {
+var _ = Describe("Tracer tests", func() {
 	addTracers := func(pers protocol.Perspective, conf *quic.Config) *quic.Config {
 		enableQlog := mrand.Int()%3 != 0
 		enableCustomTracer := mrand.Int()%3 != 0
@@ -30,10 +30,10 @@ var _ = Describe("Handshake tests", func() {
 		if enableQlog {
 			tracerConstructors = append(tracerConstructors, func(_ context.Context, p logging.Perspective, connID quic.ConnectionID) *logging.ConnectionTracer {
 				if mrand.Int()%2 == 0 { // simulate that a qlog collector might only want to log some connections
-					fmt.Fprintf(GinkgoWriter, "%s qlog tracer deciding to not trace connection %x\n", p, connID)
+					fmt.Fprintf(GinkgoWriter, "%s qlog tracer deciding to not trace connection %s\n", p, connID)
 					return nil
 				}
-				fmt.Fprintf(GinkgoWriter, "%s qlog tracing connection %x\n", p, connID)
+				fmt.Fprintf(GinkgoWriter, "%s qlog tracing connection %s\n", p, connID)
 				return qlog.NewConnectionTracer(utils.NewBufferedWriteCloser(bufio.NewWriter(&bytes.Buffer{}), io.NopCloser(nil)), p, connID)
 			})
 		}
diff --git a/integrationtests/self/uni_stream_test.go b/integrationtests/self/uni_stream_test.go
index a2fe4e50..d47df35f 100644
--- a/integrationtests/self/uni_stream_test.go
+++ b/integrationtests/self/uni_stream_test.go
@@ -142,5 +142,6 @@ var _ = Describe("Unidirectional Streams", func() {
 		runReceivingPeer(client)
 		<-done1
 		<-done2
+		client.CloseWithError(0, "")
 	})
 })
diff --git a/integrationtests/self/zero_rtt_oldgo_test.go b/integrationtests/self/zero_rtt_oldgo_test.go
index eb2302d3..aea54271 100644
--- a/integrationtests/self/zero_rtt_oldgo_test.go
+++ b/integrationtests/self/zero_rtt_oldgo_test.go
@@ -26,8 +26,8 @@ import (
 var _ = Describe("0-RTT", func() {
 	rtt := scaleDuration(5 * time.Millisecond)
 
-	runCountingProxy := func(serverPort int) (*quicproxy.QuicProxy, *uint32) {
-		var num0RTTPackets uint32 // to be used as an atomic
+	runCountingProxy := func(serverPort int) (*quicproxy.QuicProxy, *atomic.Uint32) {
+		var num0RTTPackets atomic.Uint32
 		proxy, err := quicproxy.NewQuicProxy("localhost:0", &quicproxy.Opts{
 			RemoteAddr: fmt.Sprintf("localhost:%d", serverPort),
 			DelayPacket: func(_ quicproxy.Direction, data []byte) time.Duration {
@@ -38,7 +38,7 @@ var _ = Describe("0-RTT", func() {
 					hdr, _, rest, err := wire.ParsePacket(data)
 					Expect(err).ToNot(HaveOccurred())
 					if hdr.Type == protocol.PacketType0RTT {
-						atomic.AddUint32(&num0RTTPackets, 1)
+						num0RTTPackets.Add(1)
 						break
 					}
 					data = rest
@@ -257,7 +257,7 @@ var _ = Describe("0-RTT", func() {
 				Expect(numNewConnIDs).ToNot(BeZero())
 			}
 
-			num0RTT := atomic.LoadUint32(num0RTTPackets)
+			num0RTT := num0RTTPackets.Load()
 			fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 			Expect(num0RTT).ToNot(BeZero())
 			zeroRTTPackets := get0RTTPackets(counter.getRcvdLongHeaderPackets())
@@ -348,10 +348,7 @@ var _ = Describe("0-RTT", func() {
 	})
 
 	It("transfers 0-RTT data, when 0-RTT packets are lost", func() {
-		var (
-			num0RTTPackets uint32 // to be used as an atomic
-			num0RTTDropped uint32
-		)
+		var num0RTTPackets, num0RTTDropped atomic.Uint32
 
 		tlsConf, clientConf := dialAndReceiveSessionTicket(nil)
 
@@ -374,7 +371,7 @@ var _ = Describe("0-RTT", func() {
 					hdr, _, _, err := wire.ParsePacket(data)
 					Expect(err).ToNot(HaveOccurred())
 					if hdr.Type == protocol.PacketType0RTT {
-						atomic.AddUint32(&num0RTTPackets, 1)
+						num0RTTPackets.Add(1)
 					}
 				}
 				return rtt / 2
@@ -389,7 +386,7 @@ var _ = Describe("0-RTT", func() {
 					// drop 25% of the 0-RTT packets
 					drop := mrand.Intn(4) == 0
 					if drop {
-						atomic.AddUint32(&num0RTTDropped, 1)
+						num0RTTDropped.Add(1)
 					}
 					return drop
 				}
@@ -401,8 +398,8 @@ var _ = Describe("0-RTT", func() {
 
 		transfer0RTTData(ln, proxy.LocalPort(), protocol.DefaultConnectionIDLength, clientConf, nil, PRData)
 
-		num0RTT := atomic.LoadUint32(&num0RTTPackets)
-		numDropped := atomic.LoadUint32(&num0RTTDropped)
+		num0RTT := num0RTTPackets.Load()
+		numDropped := num0RTTDropped.Load()
 		fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets. Dropped %d of those.", num0RTT, numDropped)
 		Expect(numDropped).ToNot(BeZero())
 		Expect(num0RTT).ToNot(BeZero())
@@ -485,6 +482,37 @@ var _ = Describe("0-RTT", func() {
 		Expect(zeroRTTPackets[0]).To(BeNumerically(">=", protocol.PacketNumber(5)))
 	})
 
+	It("doesn't use 0-RTT when Dial is used for the resumed connection", func() {
+		tlsConf, clientConf := dialAndReceiveSessionTicket(nil)
+
+		ln, err := quic.ListenAddrEarly(
+			"localhost:0",
+			tlsConf,
+			getQuicConfig(&quic.Config{Allow0RTT: true}),
+		)
+		Expect(err).ToNot(HaveOccurred())
+		defer ln.Close()
+		proxy, num0RTTPackets := runCountingProxy(ln.Addr().(*net.UDPAddr).Port)
+		defer proxy.Close()
+
+		conn, err := quic.DialAddr(
+			context.Background(),
+			fmt.Sprintf("localhost:%d", proxy.LocalPort()),
+			clientConf,
+			getQuicConfig(nil),
+		)
+		Expect(err).ToNot(HaveOccurred())
+		defer conn.CloseWithError(0, "")
+		Expect(conn.ConnectionState().TLS.DidResume).To(BeTrue())
+		Expect(conn.ConnectionState().Used0RTT).To(BeFalse())
+		Expect(num0RTTPackets.Load()).To(BeZero())
+
+		serverConn, err := ln.Accept(context.Background())
+		Expect(err).ToNot(HaveOccurred())
+		Expect(serverConn.ConnectionState().TLS.DidResume).To(BeTrue())
+		Expect(serverConn.ConnectionState().Used0RTT).To(BeFalse())
+	})
+
 	It("doesn't reject 0-RTT when the server's transport stream limit increased", func() {
 		const maxStreams = 1
 		tlsConf, clientConf := dialAndReceiveSessionTicket(getQuicConfig(&quic.Config{
@@ -551,7 +579,7 @@ var _ = Describe("0-RTT", func() {
 		check0RTTRejected(ln, proxy.LocalPort(), clientConf)
 
 		// The client should send 0-RTT packets, but the server doesn't process them.
-		num0RTT := atomic.LoadUint32(num0RTTPackets)
+		num0RTT := num0RTTPackets.Load()
 		fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 		Expect(num0RTT).ToNot(BeZero())
 		Expect(get0RTTPackets(counter.getRcvdLongHeaderPackets())).To(BeEmpty())
@@ -580,7 +608,7 @@ var _ = Describe("0-RTT", func() {
 		check0RTTRejected(ln, proxy.LocalPort(), clientConf)
 
 		// The client should send 0-RTT packets, but the server doesn't process them.
-		num0RTT := atomic.LoadUint32(num0RTTPackets)
+		num0RTT := num0RTTPackets.Load()
 		fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 		Expect(num0RTT).ToNot(BeZero())
 		Expect(get0RTTPackets(counter.getRcvdLongHeaderPackets())).To(BeEmpty())
@@ -607,7 +635,7 @@ var _ = Describe("0-RTT", func() {
 		check0RTTRejected(ln, proxy.LocalPort(), clientConf)
 
 		// The client should send 0-RTT packets, but the server doesn't process them.
-		num0RTT := atomic.LoadUint32(num0RTTPackets)
+		num0RTT := num0RTTPackets.Load()
 		fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 		Expect(num0RTT).ToNot(BeZero())
 		Expect(get0RTTPackets(counter.getRcvdLongHeaderPackets())).To(BeEmpty())
@@ -759,7 +787,7 @@ var _ = Describe("0-RTT", func() {
 			Expect(conn.CloseWithError(0, "")).To(Succeed())
 
 			// The client should send 0-RTT packets, but the server doesn't process them.
-			num0RTT := atomic.LoadUint32(num0RTTPackets)
+			num0RTT := num0RTTPackets.Load()
 			fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 			Expect(num0RTT).ToNot(BeZero())
 			Expect(get0RTTPackets(counter.getRcvdLongHeaderPackets())).To(BeEmpty())
@@ -830,7 +858,7 @@ var _ = Describe("0-RTT", func() {
 			defer close(received)
 			conn, err := ln.Accept(context.Background())
 			Expect(err).ToNot(HaveOccurred())
-			receivedMessage, err = conn.ReceiveMessage(context.Background())
+			receivedMessage, err = conn.ReceiveDatagram(context.Background())
 			Expect(err).ToNot(HaveOccurred())
 			Expect(conn.ConnectionState().Used0RTT).To(BeTrue())
 		}()
@@ -844,14 +872,14 @@ var _ = Describe("0-RTT", func() {
 		)
 		Expect(err).ToNot(HaveOccurred())
 		Expect(conn.ConnectionState().SupportsDatagrams).To(BeTrue())
-		Expect(conn.SendMessage(sentMessage)).To(Succeed())
+		Expect(conn.SendDatagram(sentMessage)).To(Succeed())
 		<-conn.HandshakeComplete()
 		<-received
 
 		Expect(conn.ConnectionState().Used0RTT).To(BeTrue())
 		Expect(receivedMessage).To(Equal(sentMessage))
 		Expect(conn.CloseWithError(0, "")).To(Succeed())
-		num0RTT := atomic.LoadUint32(num0RTTPackets)
+		num0RTT := num0RTTPackets.Load()
 		fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 		Expect(num0RTT).ToNot(BeZero())
 		zeroRTTPackets := get0RTTPackets(counter.getRcvdLongHeaderPackets())
@@ -884,7 +912,7 @@ var _ = Describe("0-RTT", func() {
 			defer GinkgoRecover()
 			conn, err := ln.Accept(context.Background())
 			Expect(err).ToNot(HaveOccurred())
-			_, err = conn.ReceiveMessage(context.Background())
+			_, err = conn.ReceiveDatagram(context.Background())
 			Expect(err.Error()).To(Equal("datagram support disabled"))
 			<-conn.HandshakeComplete()
 			Expect(conn.ConnectionState().Used0RTT).To(BeFalse())
@@ -900,13 +928,13 @@ var _ = Describe("0-RTT", func() {
 		Expect(err).ToNot(HaveOccurred())
 		// the client can temporarily send datagrams but the server doesn't process them.
 		Expect(conn.ConnectionState().SupportsDatagrams).To(BeTrue())
-		Expect(conn.SendMessage(make([]byte, 100))).To(Succeed())
+		Expect(conn.SendDatagram(make([]byte, 100))).To(Succeed())
 		<-conn.HandshakeComplete()
 
 		Expect(conn.ConnectionState().SupportsDatagrams).To(BeFalse())
 		Expect(conn.ConnectionState().Used0RTT).To(BeFalse())
 		Expect(conn.CloseWithError(0, "")).To(Succeed())
-		num0RTT := atomic.LoadUint32(num0RTTPackets)
+		num0RTT := num0RTTPackets.Load()
 		fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 		Expect(num0RTT).ToNot(BeZero())
 		Expect(get0RTTPackets(counter.getRcvdLongHeaderPackets())).To(BeEmpty())
diff --git a/integrationtests/self/zero_rtt_test.go b/integrationtests/self/zero_rtt_test.go
index 1f750d3a..c0fc5325 100644
--- a/integrationtests/self/zero_rtt_test.go
+++ b/integrationtests/self/zero_rtt_test.go
@@ -56,8 +56,8 @@ func (m metadataClientSessionCache) Put(key string, session *tls.ClientSessionSt
 var _ = Describe("0-RTT", func() {
 	rtt := scaleDuration(5 * time.Millisecond)
 
-	runCountingProxy := func(serverPort int) (*quicproxy.QuicProxy, *uint32) {
-		var num0RTTPackets uint32 // to be used as an atomic
+	runCountingProxy := func(serverPort int) (*quicproxy.QuicProxy, *atomic.Uint32) {
+		var num0RTTPackets atomic.Uint32
 		proxy, err := quicproxy.NewQuicProxy("localhost:0", &quicproxy.Opts{
 			RemoteAddr: fmt.Sprintf("localhost:%d", serverPort),
 			DelayPacket: func(_ quicproxy.Direction, data []byte) time.Duration {
@@ -68,7 +68,7 @@ var _ = Describe("0-RTT", func() {
 					hdr, _, rest, err := wire.ParsePacket(data)
 					Expect(err).ToNot(HaveOccurred())
 					if hdr.Type == protocol.PacketType0RTT {
-						atomic.AddUint32(&num0RTTPackets, 1)
+						num0RTTPackets.Add(1)
 						break
 					}
 					data = rest
@@ -289,7 +289,7 @@ var _ = Describe("0-RTT", func() {
 				Expect(numNewConnIDs).ToNot(BeZero())
 			}
 
-			num0RTT := atomic.LoadUint32(num0RTTPackets)
+			num0RTT := num0RTTPackets.Load()
 			fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 			Expect(num0RTT).ToNot(BeZero())
 			zeroRTTPackets := get0RTTPackets(counter.getRcvdLongHeaderPackets())
@@ -382,10 +382,7 @@ var _ = Describe("0-RTT", func() {
 	})
 
 	It("transfers 0-RTT data, when 0-RTT packets are lost", func() {
-		var (
-			num0RTTPackets uint32 // to be used as an atomic
-			num0RTTDropped uint32
-		)
+		var num0RTTPackets, num0RTTDropped atomic.Uint32
 
 		tlsConf := getTLSConfig()
 		clientConf := getTLSClientConfig()
@@ -410,7 +407,7 @@ var _ = Describe("0-RTT", func() {
 					hdr, _, _, err := wire.ParsePacket(data)
 					Expect(err).ToNot(HaveOccurred())
 					if hdr.Type == protocol.PacketType0RTT {
-						atomic.AddUint32(&num0RTTPackets, 1)
+						num0RTTPackets.Add(1)
 					}
 				}
 				return rtt / 2
@@ -425,7 +422,7 @@ var _ = Describe("0-RTT", func() {
 					// drop 25% of the 0-RTT packets
 					drop := mrand.Intn(4) == 0
 					if drop {
-						atomic.AddUint32(&num0RTTDropped, 1)
+						num0RTTDropped.Add(1)
 					}
 					return drop
 				}
@@ -437,8 +434,8 @@ var _ = Describe("0-RTT", func() {
 
 		transfer0RTTData(ln, proxy.LocalPort(), protocol.DefaultConnectionIDLength, clientConf, nil, PRData)
 
-		num0RTT := atomic.LoadUint32(&num0RTTPackets)
-		numDropped := atomic.LoadUint32(&num0RTTDropped)
+		num0RTT := num0RTTPackets.Load()
+		numDropped := num0RTTDropped.Load()
 		fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets. Dropped %d of those.", num0RTT, numDropped)
 		Expect(numDropped).ToNot(BeZero())
 		Expect(num0RTT).ToNot(BeZero())
@@ -523,6 +520,39 @@ var _ = Describe("0-RTT", func() {
 		Expect(zeroRTTPackets[0]).To(BeNumerically(">=", protocol.PacketNumber(5)))
 	})
 
+	It("doesn't use 0-RTT when Dial is used for the resumed connection", func() {
+		tlsConf := getTLSConfig()
+		clientConf := getTLSClientConfig()
+		dialAndReceiveSessionTicket(tlsConf, getQuicConfig(nil), clientConf)
+
+		ln, err := quic.ListenAddrEarly(
+			"localhost:0",
+			tlsConf,
+			getQuicConfig(&quic.Config{Allow0RTT: true}),
+		)
+		Expect(err).ToNot(HaveOccurred())
+		defer ln.Close()
+		proxy, num0RTTPackets := runCountingProxy(ln.Addr().(*net.UDPAddr).Port)
+		defer proxy.Close()
+
+		conn, err := quic.DialAddr(
+			context.Background(),
+			fmt.Sprintf("localhost:%d", proxy.LocalPort()),
+			clientConf,
+			getQuicConfig(nil),
+		)
+		Expect(err).ToNot(HaveOccurred())
+		defer conn.CloseWithError(0, "")
+		Expect(conn.ConnectionState().TLS.DidResume).To(BeTrue())
+		Expect(conn.ConnectionState().Used0RTT).To(BeFalse())
+		Expect(num0RTTPackets.Load()).To(BeZero())
+
+		serverConn, err := ln.Accept(context.Background())
+		Expect(err).ToNot(HaveOccurred())
+		Expect(serverConn.ConnectionState().TLS.DidResume).To(BeTrue())
+		Expect(serverConn.ConnectionState().Used0RTT).To(BeFalse())
+	})
+
 	It("doesn't reject 0-RTT when the server's transport stream limit increased", func() {
 		const maxStreams = 1
 		tlsConf := getTLSConfig()
@@ -594,7 +624,7 @@ var _ = Describe("0-RTT", func() {
 		check0RTTRejected(ln, proxy.LocalPort(), clientConf)
 
 		// The client should send 0-RTT packets, but the server doesn't process them.
-		num0RTT := atomic.LoadUint32(num0RTTPackets)
+		num0RTT := num0RTTPackets.Load()
 		fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 		Expect(num0RTT).ToNot(BeZero())
 		Expect(get0RTTPackets(counter.getRcvdLongHeaderPackets())).To(BeEmpty())
@@ -627,7 +657,7 @@ var _ = Describe("0-RTT", func() {
 		check0RTTRejected(ln, proxy.LocalPort(), clientConf)
 
 		// The client should send 0-RTT packets, but the server doesn't process them.
-		num0RTT := atomic.LoadUint32(num0RTTPackets)
+		num0RTT := num0RTTPackets.Load()
 		fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 		Expect(num0RTT).ToNot(BeZero())
 		Expect(get0RTTPackets(counter.getRcvdLongHeaderPackets())).To(BeEmpty())
@@ -656,7 +686,7 @@ var _ = Describe("0-RTT", func() {
 		check0RTTRejected(ln, proxy.LocalPort(), clientConf)
 
 		// The client should send 0-RTT packets, but the server doesn't process them.
-		num0RTT := atomic.LoadUint32(num0RTTPackets)
+		num0RTT := num0RTTPackets.Load()
 		fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 		Expect(num0RTT).ToNot(BeZero())
 		Expect(get0RTTPackets(counter.getRcvdLongHeaderPackets())).To(BeEmpty())
@@ -812,7 +842,7 @@ var _ = Describe("0-RTT", func() {
 			Expect(conn.CloseWithError(0, "")).To(Succeed())
 
 			// The client should send 0-RTT packets, but the server doesn't process them.
-			num0RTT := atomic.LoadUint32(num0RTTPackets)
+			num0RTT := num0RTTPackets.Load()
 			fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 			Expect(num0RTT).ToNot(BeZero())
 			Expect(get0RTTPackets(counter.getRcvdLongHeaderPackets())).To(BeEmpty())
@@ -960,7 +990,7 @@ var _ = Describe("0-RTT", func() {
 			defer close(received)
 			conn, err := ln.Accept(context.Background())
 			Expect(err).ToNot(HaveOccurred())
-			receivedMessage, err = conn.ReceiveMessage(context.Background())
+			receivedMessage, err = conn.ReceiveDatagram(context.Background())
 			Expect(err).ToNot(HaveOccurred())
 			Expect(conn.ConnectionState().Used0RTT).To(BeTrue())
 		}()
@@ -974,14 +1004,14 @@ var _ = Describe("0-RTT", func() {
 		)
 		Expect(err).ToNot(HaveOccurred())
 		Expect(conn.ConnectionState().SupportsDatagrams).To(BeTrue())
-		Expect(conn.SendMessage(sentMessage)).To(Succeed())
+		Expect(conn.SendDatagram(sentMessage)).To(Succeed())
 		<-conn.HandshakeComplete()
 		<-received
 
 		Expect(conn.ConnectionState().Used0RTT).To(BeTrue())
 		Expect(conn.CloseWithError(0, "")).To(Succeed())
 		Expect(receivedMessage).To(Equal(sentMessage))
-		num0RTT := atomic.LoadUint32(num0RTTPackets)
+		num0RTT := num0RTTPackets.Load()
 		fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 		Expect(num0RTT).ToNot(BeZero())
 		zeroRTTPackets := get0RTTPackets(counter.getRcvdLongHeaderPackets())
@@ -1016,7 +1046,7 @@ var _ = Describe("0-RTT", func() {
 			defer GinkgoRecover()
 			conn, err := ln.Accept(context.Background())
 			Expect(err).ToNot(HaveOccurred())
-			_, err = conn.ReceiveMessage(context.Background())
+			_, err = conn.ReceiveDatagram(context.Background())
 			Expect(err.Error()).To(Equal("datagram support disabled"))
 			<-conn.HandshakeComplete()
 			Expect(conn.ConnectionState().Used0RTT).To(BeFalse())
@@ -1032,13 +1062,13 @@ var _ = Describe("0-RTT", func() {
 		Expect(err).ToNot(HaveOccurred())
 		// the client can temporarily send datagrams but the server doesn't process them.
 		Expect(conn.ConnectionState().SupportsDatagrams).To(BeTrue())
-		Expect(conn.SendMessage(make([]byte, 100))).To(Succeed())
+		Expect(conn.SendDatagram(make([]byte, 100))).To(Succeed())
 		<-conn.HandshakeComplete()
 
 		Expect(conn.ConnectionState().SupportsDatagrams).To(BeFalse())
 		Expect(conn.ConnectionState().Used0RTT).To(BeFalse())
 		Expect(conn.CloseWithError(0, "")).To(Succeed())
-		num0RTT := atomic.LoadUint32(num0RTTPackets)
+		num0RTT := num0RTTPackets.Load()
 		fmt.Fprintf(GinkgoWriter, "Sent %d 0-RTT packets.", num0RTT)
 		Expect(num0RTT).ToNot(BeZero())
 		Expect(get0RTTPackets(counter.getRcvdLongHeaderPackets())).To(BeEmpty())
diff --git a/integrationtests/tools/proxy/proxy_test.go b/integrationtests/tools/proxy/proxy_test.go
index c9b80be5..c51a436a 100644
--- a/integrationtests/tools/proxy/proxy_test.go
+++ b/integrationtests/tools/proxy/proxy_test.go
@@ -141,7 +141,7 @@ var _ = Describe("QUIC Proxy", func() {
 	Context("Proxy tests", func() {
 		var (
 			serverConn            *net.UDPConn
-			serverNumPacketsSent  int32
+			serverNumPacketsSent  atomic.Int32
 			serverReceivedPackets chan packetData
 			clientConn            *net.UDPConn
 			proxy                 *QuicProxy
@@ -159,9 +159,9 @@ var _ = Describe("QUIC Proxy", func() {
 		BeforeEach(func() {
 			stoppedReading = make(chan struct{})
 			serverReceivedPackets = make(chan packetData, 100)
-			atomic.StoreInt32(&serverNumPacketsSent, 0)
+			serverNumPacketsSent.Store(0)
 
-			// setup a dump UDP server
+			// set up a dump UDP server
 			// in production this would be a QUIC server
 			raddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
 			Expect(err).ToNot(HaveOccurred())
@@ -181,7 +181,7 @@ var _ = Describe("QUIC Proxy", func() {
 					data := buf[0:n]
 					serverReceivedPackets <- packetData(data)
 					// echo the packet
-					atomic.AddInt32(&serverNumPacketsSent, 1)
+					serverNumPacketsSent.Add(1)
 					serverConn.WriteToUDP(data, addr)
 				}
 			}()
@@ -236,7 +236,7 @@ var _ = Describe("QUIC Proxy", func() {
 				}()
 
 				Eventually(serverReceivedPackets).Should(HaveLen(2))
-				Expect(atomic.LoadInt32(&serverNumPacketsSent)).To(BeEquivalentTo(2))
+				Expect(serverNumPacketsSent.Load()).To(BeEquivalentTo(2))
 				Eventually(clientReceivedPackets).Should(HaveLen(2))
 				Expect(string(<-clientReceivedPackets)).To(ContainSubstring("foobar"))
 				Expect(string(<-clientReceivedPackets)).To(ContainSubstring("decafbad"))
@@ -245,14 +245,14 @@ var _ = Describe("QUIC Proxy", func() {
 
 		Context("Drop Callbacks", func() {
 			It("drops incoming packets", func() {
-				var counter int32
+				var counter atomic.Int32
 				opts := &Opts{
 					RemoteAddr: serverConn.LocalAddr().String(),
 					DropPacket: func(d Direction, _ []byte) bool {
 						if d != DirectionIncoming {
 							return false
 						}
-						return atomic.AddInt32(&counter, 1)%2 == 1
+						return counter.Add(1)%2 == 1
 					},
 				}
 				startProxy(opts)
@@ -267,14 +267,14 @@ var _ = Describe("QUIC Proxy", func() {
 
 			It("drops outgoing packets", func() {
 				const numPackets = 6
-				var counter int32
+				var counter atomic.Int32
 				opts := &Opts{
 					RemoteAddr: serverConn.LocalAddr().String(),
 					DropPacket: func(d Direction, _ []byte) bool {
 						if d != DirectionOutgoing {
 							return false
 						}
-						return atomic.AddInt32(&counter, 1)%2 == 1
+						return counter.Add(1)%2 == 1
 					},
 				}
 				startProxy(opts)
@@ -315,7 +315,7 @@ var _ = Describe("QUIC Proxy", func() {
 			}
 
 			It("delays incoming packets", func() {
-				var counter int32
+				var counter atomic.Int32
 				opts := &Opts{
 					RemoteAddr: serverConn.LocalAddr().String(),
 					// delay packet 1 by 200 ms
@@ -325,7 +325,7 @@ var _ = Describe("QUIC Proxy", func() {
 						if d == DirectionOutgoing {
 							return 0
 						}
-						p := atomic.AddInt32(&counter, 1)
+						p := counter.Add(1)
 						return time.Duration(p) * delay
 					},
 				}
@@ -349,7 +349,7 @@ var _ = Describe("QUIC Proxy", func() {
 			})
 
 			It("handles reordered packets", func() {
-				var counter int32
+				var counter atomic.Int32
 				opts := &Opts{
 					RemoteAddr: serverConn.LocalAddr().String(),
 					// delay packet 1 by 600 ms
@@ -359,7 +359,7 @@ var _ = Describe("QUIC Proxy", func() {
 						if d == DirectionOutgoing {
 							return 0
 						}
-						p := atomic.AddInt32(&counter, 1)
+						p := counter.Add(1)
 						return 600*time.Millisecond - time.Duration(p-1)*delay
 					},
 				}
@@ -407,7 +407,7 @@ var _ = Describe("QUIC Proxy", func() {
 
 			It("delays outgoing packets", func() {
 				const numPackets = 3
-				var counter int32
+				var counter atomic.Int32
 				opts := &Opts{
 					RemoteAddr: serverConn.LocalAddr().String(),
 					// delay packet 1 by 200 ms
@@ -417,7 +417,7 @@ var _ = Describe("QUIC Proxy", func() {
 						if d == DirectionIncoming {
 							return 0
 						}
-						p := atomic.AddInt32(&counter, 1)
+						p := counter.Add(1)
 						return time.Duration(p) * delay
 					},
 				}
diff --git a/internal/handshake/crypto_setup_test.go b/internal/handshake/crypto_setup_test.go
index 9b2844f7..4e92118a 100644
--- a/internal/handshake/crypto_setup_test.go
+++ b/internal/handshake/crypto_setup_test.go
@@ -8,6 +8,7 @@ import (
 	"crypto/x509/pkix"
 	"math/big"
 	"net"
+	"reflect"
 	"runtime"
 	"strings"
 	"time"
@@ -139,32 +140,43 @@ var _ = Describe("Crypto Setup TLS", func() {
 				},
 			}
 			addConnToClientHelloInfo(tlsConf, local, remote)
-			_, err := tlsConf.GetConfigForClient(&tls.ClientHelloInfo{})
+			conf, err := tlsConf.GetConfigForClient(&tls.ClientHelloInfo{})
 			Expect(err).ToNot(HaveOccurred())
 			Expect(localAddr).To(Equal(local))
 			Expect(remoteAddr).To(Equal(remote))
+			Expect(conf).ToNot(BeNil())
+			Expect(conf.MinVersion).To(BeEquivalentTo(tls.VersionTLS13))
 		})
 
 		It("wraps GetConfigForClient, recursively", func() {
 			var localAddr, remoteAddr net.Addr
 			tlsConf := &tls.Config{}
+			var innerConf *tls.Config
+			getCert := func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { //nolint:unparam
+				localAddr = info.Conn.LocalAddr()
+				remoteAddr = info.Conn.RemoteAddr()
+				cert := generateCert()
+				return &cert, nil
+			}
 			tlsConf.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
-				conf := tlsConf.Clone()
-				conf.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
-					localAddr = info.Conn.LocalAddr()
-					remoteAddr = info.Conn.RemoteAddr()
-					cert := generateCert()
-					return &cert, nil
-				}
-				return conf, nil
+				innerConf = tlsConf.Clone()
+				// set the MaxVersion, so we can check that quic-go doesn't overwrite the user's config
+				innerConf.MaxVersion = tls.VersionTLS12
+				innerConf.GetCertificate = getCert
+				return innerConf, nil
 			}
 			addConnToClientHelloInfo(tlsConf, local, remote)
 			conf, err := tlsConf.GetConfigForClient(&tls.ClientHelloInfo{})
 			Expect(err).ToNot(HaveOccurred())
+			Expect(conf).ToNot(BeNil())
+			Expect(conf.MinVersion).To(BeEquivalentTo(tls.VersionTLS13))
 			_, err = conf.GetCertificate(&tls.ClientHelloInfo{})
 			Expect(err).ToNot(HaveOccurred())
 			Expect(localAddr).To(Equal(local))
 			Expect(remoteAddr).To(Equal(remote))
+			// make sure that the tls.Config returned by GetConfigForClient isn't modified
+			Expect(reflect.ValueOf(innerConf.GetCertificate).Pointer() == reflect.ValueOf(getCert).Pointer()).To(BeTrue())
+			Expect(innerConf.MaxVersion).To(BeEquivalentTo(tls.VersionTLS12))
 		})
 	})
 
@@ -450,8 +462,8 @@ var _ = Describe("Crypto Setup TLS", func() {
 				Eventually(receivedSessionTicket).Should(BeClosed())
 				Expect(server.ConnectionState().DidResume).To(BeTrue())
 				Expect(client.ConnectionState().DidResume).To(BeTrue())
-				Expect(clientRTTStats.SmoothedRTT()).To(Equal(clientRTT))
 				if !strings.Contains(runtime.Version(), "go1.20") {
+					Expect(clientRTTStats.SmoothedRTT()).To(Equal(clientRTT))
 					Expect(serverRTTStats.SmoothedRTT()).To(Equal(serverRTT))
 				}
 			})
diff --git a/internal/qtls/client_session_cache_test.go b/internal/qtls/client_session_cache_test.go
index a299551a..6af19293 100644
--- a/internal/qtls/client_session_cache_test.go
+++ b/internal/qtls/client_session_cache_test.go
@@ -41,7 +41,10 @@ var _ = Describe("Client Session Cache", func() {
 			ClientSessionCache: &clientSessionCache{
 				wrapped: tls.NewLRUClientSessionCache(10),
 				getData: func() []byte { return []byte("session") },
-				setData: func(data []byte) { restored <- data },
+				setData: func(data []byte) bool {
+					restored <- data
+					return true
+				},
 			},
 		}
 		conn, err := tls.Dial(
diff --git a/mock_packet_handler_manager_test.go b/mock_packet_handler_manager_test.go
index fafd43c9..e3f1e00c 100644
--- a/mock_packet_handler_manager_test.go
+++ b/mock_packet_handler_manager_test.go
@@ -186,42 +186,6 @@ func (c *PacketHandlerManagerCloseCall) DoAndReturn(f func(error)) *PacketHandle
 	return c
 }
 
-// CloseServer mocks base method.
-func (m *MockPacketHandlerManager) CloseServer() {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "CloseServer")
-}
-
-// CloseServer indicates an expected call of CloseServer.
-func (mr *MockPacketHandlerManagerMockRecorder) CloseServer() *PacketHandlerManagerCloseServerCall {
-	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseServer", reflect.TypeOf((*MockPacketHandlerManager)(nil).CloseServer))
-	return &PacketHandlerManagerCloseServerCall{Call: call}
-}
-
-// PacketHandlerManagerCloseServerCall wrap *gomock.Call
-type PacketHandlerManagerCloseServerCall struct {
-	*gomock.Call
-}
-
-// Return rewrite *gomock.Call.Return
-func (c *PacketHandlerManagerCloseServerCall) Return() *PacketHandlerManagerCloseServerCall {
-	c.Call = c.Call.Return()
-	return c
-}
-
-// Do rewrite *gomock.Call.Do
-func (c *PacketHandlerManagerCloseServerCall) Do(f func()) *PacketHandlerManagerCloseServerCall {
-	c.Call = c.Call.Do(f)
-	return c
-}
-
-// DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *PacketHandlerManagerCloseServerCall) DoAndReturn(f func()) *PacketHandlerManagerCloseServerCall {
-	c.Call = c.Call.DoAndReturn(f)
-	return c
-}
-
 // Get mocks base method.
 func (m *MockPacketHandlerManager) Get(arg0 protocol.ConnectionID) (packetHandler, bool) {
 	m.ctrl.T.Helper()
diff --git a/mock_quic_conn_test.go b/mock_quic_conn_test.go
index c7d84850..2a677066 100644
--- a/mock_quic_conn_test.go
+++ b/mock_quic_conn_test.go
@@ -541,41 +541,41 @@ func (c *QUICConnOpenUniStreamSyncCall) DoAndReturn(f func(context.Context) (Sen
 	return c
 }
 
-// ReceiveMessage mocks base method.
-func (m *MockQUICConn) ReceiveMessage(arg0 context.Context) ([]byte, error) {
+// ReceiveDatagram mocks base method.
+func (m *MockQUICConn) ReceiveDatagram(arg0 context.Context) ([]byte, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ReceiveMessage", arg0)
+	ret := m.ctrl.Call(m, "ReceiveDatagram", arg0)
 	ret0, _ := ret[0].([]byte)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// ReceiveMessage indicates an expected call of ReceiveMessage.
-func (mr *MockQUICConnMockRecorder) ReceiveMessage(arg0 any) *QUICConnReceiveMessageCall {
+// ReceiveDatagram indicates an expected call of ReceiveDatagram.
+func (mr *MockQUICConnMockRecorder) ReceiveDatagram(arg0 any) *QUICConnReceiveDatagramCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceiveMessage", reflect.TypeOf((*MockQUICConn)(nil).ReceiveMessage), arg0)
-	return &QUICConnReceiveMessageCall{Call: call}
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceiveDatagram", reflect.TypeOf((*MockQUICConn)(nil).ReceiveDatagram), arg0)
+	return &QUICConnReceiveDatagramCall{Call: call}
 }
 
-// QUICConnReceiveMessageCall wrap *gomock.Call
-type QUICConnReceiveMessageCall struct {
+// QUICConnReceiveDatagramCall wrap *gomock.Call
+type QUICConnReceiveDatagramCall struct {
 	*gomock.Call
 }
 
 // Return rewrite *gomock.Call.Return
-func (c *QUICConnReceiveMessageCall) Return(arg0 []byte, arg1 error) *QUICConnReceiveMessageCall {
+func (c *QUICConnReceiveDatagramCall) Return(arg0 []byte, arg1 error) *QUICConnReceiveDatagramCall {
 	c.Call = c.Call.Return(arg0, arg1)
 	return c
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *QUICConnReceiveMessageCall) Do(f func(context.Context) ([]byte, error)) *QUICConnReceiveMessageCall {
+func (c *QUICConnReceiveDatagramCall) Do(f func(context.Context) ([]byte, error)) *QUICConnReceiveDatagramCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *QUICConnReceiveMessageCall) DoAndReturn(f func(context.Context) ([]byte, error)) *QUICConnReceiveMessageCall {
+func (c *QUICConnReceiveDatagramCall) DoAndReturn(f func(context.Context) ([]byte, error)) *QUICConnReceiveDatagramCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
@@ -618,40 +618,40 @@ func (c *QUICConnRemoteAddrCall) DoAndReturn(f func() net.Addr) *QUICConnRemoteA
 	return c
 }
 
-// SendMessage mocks base method.
-func (m *MockQUICConn) SendMessage(arg0 []byte) error {
+// SendDatagram mocks base method.
+func (m *MockQUICConn) SendDatagram(arg0 []byte) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "SendMessage", arg0)
+	ret := m.ctrl.Call(m, "SendDatagram", arg0)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// SendMessage indicates an expected call of SendMessage.
-func (mr *MockQUICConnMockRecorder) SendMessage(arg0 any) *QUICConnSendMessageCall {
+// SendDatagram indicates an expected call of SendDatagram.
+func (mr *MockQUICConnMockRecorder) SendDatagram(arg0 any) *QUICConnSendDatagramCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockQUICConn)(nil).SendMessage), arg0)
-	return &QUICConnSendMessageCall{Call: call}
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendDatagram", reflect.TypeOf((*MockQUICConn)(nil).SendDatagram), arg0)
+	return &QUICConnSendDatagramCall{Call: call}
 }
 
-// QUICConnSendMessageCall wrap *gomock.Call
-type QUICConnSendMessageCall struct {
+// QUICConnSendDatagramCall wrap *gomock.Call
+type QUICConnSendDatagramCall struct {
 	*gomock.Call
 }
 
 // Return rewrite *gomock.Call.Return
-func (c *QUICConnSendMessageCall) Return(arg0 error) *QUICConnSendMessageCall {
+func (c *QUICConnSendDatagramCall) Return(arg0 error) *QUICConnSendDatagramCall {
 	c.Call = c.Call.Return(arg0)
 	return c
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *QUICConnSendMessageCall) Do(f func([]byte) error) *QUICConnSendMessageCall {
+func (c *QUICConnSendDatagramCall) Do(f func([]byte) error) *QUICConnSendDatagramCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *QUICConnSendMessageCall) DoAndReturn(f func([]byte) error) *QUICConnSendMessageCall {
+func (c *QUICConnSendDatagramCall) DoAndReturn(f func([]byte) error) *QUICConnSendDatagramCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/packet_handler_map_test.go b/packet_handler_map_test.go
index 24cef871..d40c395f 100644
--- a/packet_handler_map_test.go
+++ b/packet_handler_map_test.go
@@ -159,23 +159,6 @@ var _ = Describe("Packet Handler Map", func() {
 		Eventually(func() bool { _, ok := m.Get(connID); return ok }).Should(BeFalse())
 	})
 
-	It("closes the server", func() {
-		m := newPacketHandlerMap(nil, nil, utils.DefaultLogger)
-		for i := 0; i < 10; i++ {
-			conn := NewMockPacketHandler(mockCtrl)
-			if i%2 == 0 {
-				conn.EXPECT().getPerspective().Return(protocol.PerspectiveClient)
-			} else {
-				conn.EXPECT().getPerspective().Return(protocol.PerspectiveServer)
-				conn.EXPECT().shutdown()
-			}
-			b := make([]byte, 12)
-			rand.Read(b)
-			m.Add(protocol.ParseConnectionID(b), conn)
-		}
-		m.CloseServer()
-	})
-
 	It("closes", func() {
 		m := newPacketHandlerMap(nil, nil, utils.DefaultLogger)
 		testErr := errors.New("shutdown")
diff --git a/server_test.go b/server_test.go
index 6b250c33..eeea3b8c 100644
--- a/server_test.go
+++ b/server_test.go
@@ -326,6 +326,8 @@ var _ = Describe("Server", func() {
 				// make sure we're using a server-generated connection ID
 				Eventually(run).Should(BeClosed())
 				Eventually(done).Should(BeClosed())
+				// shutdown
+				conn.EXPECT().destroy(gomock.Any())
 			})
 
 			It("sends a Version Negotiation Packet for unsupported versions", func() {
@@ -527,6 +529,8 @@ var _ = Describe("Server", func() {
 				// make sure we're using a server-generated connection ID
 				Eventually(run).Should(BeClosed())
 				Eventually(done).Should(BeClosed())
+				// shutdown
+				conn.EXPECT().destroy(gomock.Any())
 			})
 
 			It("drops packets if the receive queue is full", func() {
@@ -565,6 +569,8 @@ var _ = Describe("Server", func() {
 					conn.EXPECT().run().MaxTimes(1)
 					conn.EXPECT().Context().Return(context.Background()).MaxTimes(1)
 					conn.EXPECT().HandshakeComplete().Return(make(chan struct{})).MaxTimes(1)
+					// shutdown
+					conn.EXPECT().destroy(gomock.Any()).MaxTimes(1)
 					return conn
 				}
 
@@ -956,30 +962,69 @@ var _ = Describe("Server", func() {
 		})
 
 		Context("accepting connections", func() {
-			It("returns Accept when an error occurs", func() {
-				testErr := errors.New("test err")
-
+			It("returns Accept when closed", func() {
 				done := make(chan struct{})
 				go func() {
 					defer GinkgoRecover()
 					_, err := serv.Accept(context.Background())
-					Expect(err).To(MatchError(testErr))
+					Expect(err).To(MatchError(ErrServerClosed))
 					close(done)
 				}()
 
-				serv.setCloseError(testErr)
+				serv.Close()
 				Eventually(done).Should(BeClosed())
-				serv.onClose() // shutdown
 			})
 
 			It("returns immediately, if an error occurred before", func() {
-				testErr := errors.New("test err")
-				serv.setCloseError(testErr)
+				serv.Close()
 				for i := 0; i < 3; i++ {
 					_, err := serv.Accept(context.Background())
-					Expect(err).To(MatchError(testErr))
+					Expect(err).To(MatchError(ErrServerClosed))
 				}
-				serv.onClose() // shutdown
+			})
+
+			It("closes connection that are still handshaking after Close", func() {
+				serv.Close()
+
+				destroyed := make(chan struct{})
+				serv.newConn = func(
+					_ sendConn,
+					_ connRunner,
+					_ protocol.ConnectionID,
+					_ *protocol.ConnectionID,
+					_ protocol.ConnectionID,
+					_ protocol.ConnectionID,
+					_ protocol.ConnectionID,
+					_ ConnectionIDGenerator,
+					_ protocol.StatelessResetToken,
+					conf *Config,
+					_ *tls.Config,
+					_ *handshake.TokenGenerator,
+					_ bool,
+					_ *logging.ConnectionTracer,
+					_ uint64,
+					_ utils.Logger,
+					_ protocol.VersionNumber,
+				) quicConn {
+					conn := NewMockQUICConn(mockCtrl)
+					conn.EXPECT().handlePacket(gomock.Any())
+					conn.EXPECT().destroy(&qerr.TransportError{ErrorCode: ConnectionRefused}).Do(func(error) { close(destroyed) })
+					conn.EXPECT().HandshakeComplete().Return(make(chan struct{}))
+					conn.EXPECT().run().MaxTimes(1)
+					conn.EXPECT().Context().Return(context.Background())
+					return conn
+				}
+				phm.EXPECT().Get(gomock.Any())
+				phm.EXPECT().AddWithConnID(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_, _ protocol.ConnectionID, fn func() (packetHandler, bool)) bool {
+					phm.EXPECT().GetStatelessResetToken(gomock.Any())
+					_, ok := fn()
+					return ok
+				})
+				serv.handleInitialImpl(
+					receivedPacket{buffer: getPacketBuffer()},
+					&wire.Header{DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7, 8})},
+				)
+				Eventually(destroyed).Should(BeClosed())
 			})
 
 			It("returns when the context is canceled", func() {
@@ -1343,10 +1388,7 @@ var _ = Describe("Server", func() {
 			serv.connHandler = phm
 		})
 
-		AfterEach(func() {
-			phm.EXPECT().CloseServer().MaxTimes(1)
-			tr.Close()
-		})
+		AfterEach(func() { tr.Close() })
 
 		It("passes packets to existing connections", func() {
 			connID := protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7, 8})
@@ -1425,6 +1467,8 @@ var _ = Describe("Server", func() {
 				conn.EXPECT().earlyConnReady()
 				conn.EXPECT().Context().Return(context.Background())
 				close(called)
+				// shutdown
+				conn.EXPECT().destroy(gomock.Any())
 				return conn
 			}
 
diff --git a/transport_test.go b/transport_test.go
index 5fa63213..c93d1da9 100644
--- a/transport_test.go
+++ b/transport_test.go
@@ -114,7 +114,6 @@ var _ = Describe("Transport", func() {
 		phm := NewMockPacketHandlerManager(mockCtrl)
 		tr.handlerMap = phm
 
-		phm.EXPECT().CloseServer()
 		Expect(ln.Close()).To(Succeed())
 
 		// shutdown