quic: use testing/synctest

Replace bespoke fake time and synchronization with testing/synctest.

Change-Id: Ic3fe9635dbad36c890783c38e00708c6cb7a15f8
Reviewed-on: https://go-review.googlesource.com/c/net/+/714482
Reviewed-by: Nicholas Husin <nsh@golang.org>
Reviewed-by: Nicholas Husin <husin@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Damien Neil <dneil@google.com>
diff --git a/quic/bench_test.go b/quic/bench_test.go
index 9d8e5d2..002b40e 100644
--- a/quic/bench_test.go
+++ b/quic/bench_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
diff --git a/quic/config_test.go b/quic/config_test.go
index 3511cd4..df878da 100644
--- a/quic/config_test.go
+++ b/quic/config_test.go
@@ -2,11 +2,19 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
-import "testing"
+import (
+	"testing"
+	"testing/synctest"
+)
 
 func TestConfigTransportParameters(t *testing.T) {
+	synctest.Test(t, testConfigTransportParameters)
+}
+func testConfigTransportParameters(t *testing.T) {
 	const (
 		wantInitialMaxData        = int64(1)
 		wantInitialMaxStreamData  = int64(2)
diff --git a/quic/conn.go b/quic/conn.go
index 40bdddc..fd812b8 100644
--- a/quic/conn.go
+++ b/quic/conn.go
@@ -69,23 +69,12 @@
 	// init is called after a conn is created.
 	init(first bool)
 
-	// nextMessage is called to request the next event from msgc.
-	// Used to give tests control of the connection event loop.
-	nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any)
-
 	// handleTLSEvent is called with each TLS event.
 	handleTLSEvent(tls.QUICEvent)
 
 	// newConnID is called to generate a new connection ID.
 	// Permits tests to generate consistent connection IDs rather than random ones.
 	newConnID(seq int64) ([]byte, error)
-
-	// waitUntil blocks until the until func returns true or the context is done.
-	// Used to synchronize asynchronous blocking operations in tests.
-	waitUntil(ctx context.Context, until func() bool) error
-
-	// timeNow returns the current time.
-	timeNow() time.Time
 }
 
 // newServerConnIDs is connection IDs associated with a new server connection.
@@ -102,7 +91,6 @@
 		endpoint:             e,
 		config:               config,
 		peerAddr:             unmapAddrPort(peerAddr),
-		msgc:                 make(chan any, 1),
 		donec:                make(chan struct{}),
 		peerAckDelayExponent: -1,
 	}
@@ -299,17 +287,12 @@
 	// The connection timer sends a message to the connection loop on expiry.
 	// We need to give it an expiry when creating it, so set the initial timeout to
 	// an arbitrary large value. The timer will be reset before this expires (and it
-	// isn't a problem if it does anyway). Skip creating the timer in tests which
-	// take control of the connection message loop.
-	var timer *time.Timer
+	// isn't a problem if it does anyway).
 	var lastTimeout time.Time
-	hooks := c.testHooks
-	if hooks == nil {
-		timer = time.AfterFunc(1*time.Hour, func() {
-			c.sendMsg(timerEvent{})
-		})
-		defer timer.Stop()
-	}
+	timer := time.AfterFunc(1*time.Hour, func() {
+		c.sendMsg(timerEvent{})
+	})
+	defer timer.Stop()
 
 	for c.lifetime.state != connStateDone {
 		sendTimeout := c.maybeSend(now) // try sending
@@ -326,10 +309,7 @@
 		}
 
 		var m any
-		if hooks != nil {
-			// Tests only: Wait for the test to tell us to continue.
-			now, m = hooks.nextMessage(c.msgc, nextTimeout)
-		} else if !nextTimeout.IsZero() && nextTimeout.Before(now) {
+		if !nextTimeout.IsZero() && nextTimeout.Before(now) {
 			// A connection timer has expired.
 			now = time.Now()
 			m = timerEvent{}
@@ -372,6 +352,9 @@
 		case func(time.Time, *Conn):
 			// Send a func to msgc to run it on the main Conn goroutine
 			m(now, c)
+		case func(now, next time.Time, _ *Conn):
+			// Send a func to msgc to run it on the main Conn goroutine
+			m(now, nextTimeout, c)
 		default:
 			panic(fmt.Sprintf("quic: unrecognized conn message %T", m))
 		}
@@ -410,31 +393,7 @@
 		defer close(donec)
 		f(now, c)
 	}
-	if c.testHooks != nil {
-		// In tests, we can't rely on being able to send a message immediately:
-		// c.msgc might be full, and testConnHooks.nextMessage might be waiting
-		// for us to block before it processes the next message.
-		// To avoid a deadlock, we send the message in waitUntil.
-		// If msgc is empty, the message is buffered.
-		// If msgc is full, we block and let nextMessage process the queue.
-		msgc := c.msgc
-		c.testHooks.waitUntil(ctx, func() bool {
-			for {
-				select {
-				case msgc <- msg:
-					msgc = nil // send msg only once
-				case <-donec:
-					return true
-				case <-c.donec:
-					return true
-				default:
-					return false
-				}
-			}
-		})
-	} else {
-		c.sendMsg(msg)
-	}
+	c.sendMsg(msg)
 	select {
 	case <-donec:
 	case <-c.donec:
@@ -444,16 +403,6 @@
 }
 
 func (c *Conn) waitOnDone(ctx context.Context, ch <-chan struct{}) error {
-	if c.testHooks != nil {
-		return c.testHooks.waitUntil(ctx, func() bool {
-			select {
-			case <-ch:
-				return true
-			default:
-			}
-			return false
-		})
-	}
 	// Check the channel before the context.
 	// We always prefer to return results when available,
 	// even when provided with an already-canceled context.
diff --git a/quic/conn_async_test.go b/quic/conn_async_test.go
index f261e90..08cc7d3 100644
--- a/quic/conn_async_test.go
+++ b/quic/conn_async_test.go
@@ -2,44 +2,21 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
 	"context"
 	"errors"
 	"fmt"
-	"path/filepath"
-	"runtime"
-	"sync"
+	"testing/synctest"
 )
 
-// asyncTestState permits handling asynchronous operations in a synchronous test.
-//
-// For example, a test may want to write to a stream and observe that
-// STREAM frames are sent with the contents of the write in response
-// to MAX_STREAM_DATA frames received from the peer.
-// The Stream.Write is an asynchronous operation, but the test is simpler
-// if we can start the write, observe the first STREAM frame sent,
-// send a MAX_STREAM_DATA frame, observe the next STREAM frame sent, etc.
-//
-// We do this by instrumenting points where operations can block.
-// We start async operations like Write in a goroutine,
-// and wait for the operation to either finish or hit a blocking point.
-// When the connection event loop is idle, we check a list of
-// blocked operations to see if any can be woken.
-type asyncTestState struct {
-	mu      sync.Mutex
-	notify  chan struct{}
-	blocked map[*blockedAsync]struct{}
-}
-
 // An asyncOp is an asynchronous operation that results in (T, error).
 type asyncOp[T any] struct {
-	v   T
-	err error
-
-	caller     string
-	tc         *testConn
+	v          T
+	err        error
 	donec      chan struct{}
 	cancelFunc context.CancelFunc
 }
@@ -47,17 +24,18 @@
 // cancel cancels the async operation's context, and waits for
 // the operation to complete.
 func (a *asyncOp[T]) cancel() {
+	synctest.Wait()
 	select {
 	case <-a.donec:
 		return // already done
 	default:
 	}
 	a.cancelFunc()
-	<-a.tc.asyncTestState.notify
+	synctest.Wait()
 	select {
 	case <-a.donec:
 	default:
-		panic(fmt.Errorf("%v: async op failed to finish after being canceled", a.caller))
+		panic(fmt.Errorf("async op failed to finish after being canceled"))
 	}
 }
 
@@ -71,115 +49,30 @@
 // control over the progress of operations, an asyncOp can only
 // become done in reaction to the test taking some action.
 func (a *asyncOp[T]) result() (v T, err error) {
-	a.tc.wait()
+	synctest.Wait()
 	select {
 	case <-a.donec:
 		return a.v, a.err
 	default:
-		return v, errNotDone
+		return a.v, errNotDone
 	}
 }
 
-// A blockedAsync is a blocked async operation.
-type blockedAsync struct {
-	until func() bool   // when this returns true, the operation is unblocked
-	donec chan struct{} // closed when the operation is unblocked
-}
-
-type asyncContextKey struct{}
-
 // runAsync starts an asynchronous operation.
 //
 // The function f should call a blocking function such as
 // Stream.Write or Conn.AcceptStream and return its result.
 // It must use the provided context.
 func runAsync[T any](tc *testConn, f func(context.Context) (T, error)) *asyncOp[T] {
-	as := &tc.asyncTestState
-	if as.notify == nil {
-		as.notify = make(chan struct{})
-		as.mu.Lock()
-		as.blocked = make(map[*blockedAsync]struct{})
-		as.mu.Unlock()
-	}
-	_, file, line, _ := runtime.Caller(1)
-	ctx := context.WithValue(context.Background(), asyncContextKey{}, true)
-	ctx, cancel := context.WithCancel(ctx)
+	ctx, cancel := context.WithCancel(tc.t.Context())
 	a := &asyncOp[T]{
-		tc:         tc,
-		caller:     fmt.Sprintf("%v:%v", filepath.Base(file), line),
 		donec:      make(chan struct{}),
 		cancelFunc: cancel,
 	}
 	go func() {
+		defer close(a.donec)
 		a.v, a.err = f(ctx)
-		close(a.donec)
-		as.notify <- struct{}{}
 	}()
-	tc.t.Cleanup(func() {
-		if _, err := a.result(); err == errNotDone {
-			tc.t.Errorf("%v: async operation is still executing at end of test", a.caller)
-			a.cancel()
-		}
-	})
-	// Wait for the operation to either finish or block.
-	<-as.notify
-	tc.wait()
+	synctest.Wait()
 	return a
 }
-
-// waitUntil waits for a blocked async operation to complete.
-// The operation is complete when the until func returns true.
-func (as *asyncTestState) waitUntil(ctx context.Context, until func() bool) error {
-	if until() {
-		return nil
-	}
-	if err := ctx.Err(); err != nil {
-		// Context has already expired.
-		return err
-	}
-	if ctx.Value(asyncContextKey{}) == nil {
-		// Context is not one that we've created, and hasn't expired.
-		// This probably indicates that we've tried to perform a
-		// blocking operation without using the async test harness here,
-		// which may have unpredictable results.
-		panic("blocking async point with unexpected Context")
-	}
-	b := &blockedAsync{
-		until: until,
-		donec: make(chan struct{}),
-	}
-	// Record this as a pending blocking operation.
-	as.mu.Lock()
-	as.blocked[b] = struct{}{}
-	as.mu.Unlock()
-	// Notify the creator of the operation that we're blocked,
-	// and wait to be woken up.
-	as.notify <- struct{}{}
-	select {
-	case <-b.donec:
-	case <-ctx.Done():
-		return ctx.Err()
-	}
-	return nil
-}
-
-// wakeAsync tries to wake up a blocked async operation.
-// It returns true if one was woken, false otherwise.
-func (as *asyncTestState) wakeAsync() bool {
-	as.mu.Lock()
-	var woken *blockedAsync
-	for w := range as.blocked {
-		if w.until() {
-			woken = w
-			delete(as.blocked, w)
-			break
-		}
-	}
-	as.mu.Unlock()
-	if woken == nil {
-		return false
-	}
-	close(woken.donec)
-	<-as.notify // must not hold as.mu while blocked here
-	return true
-}
diff --git a/quic/conn_close_test.go b/quic/conn_close_test.go
index 0b37b3e..472a8f2 100644
--- a/quic/conn_close_test.go
+++ b/quic/conn_close_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -9,10 +11,14 @@
 	"crypto/tls"
 	"errors"
 	"testing"
+	"testing/synctest"
 	"time"
 )
 
 func TestConnCloseResponseBackoff(t *testing.T) {
+	synctest.Test(t, testConnCloseResponseBackoff)
+}
+func testConnCloseResponseBackoff(t *testing.T) {
 	tc := newTestConn(t, clientSide, func(c *Config) {
 		clear(c.StatelessResetKey[:])
 	})
@@ -34,18 +40,18 @@
 	tc.writeFrames(packetType1RTT, debugFramePing{})
 	tc.wantIdle("packets received immediately after CONN_CLOSE receive no response")
 
-	tc.advance(1100 * time.Microsecond)
+	time.Sleep(1100 * time.Microsecond)
 	tc.writeFrames(packetType1RTT, debugFramePing{})
 	tc.wantFrame("receiving packet 1.1ms after CONN_CLOSE generates another CONN_CLOSE",
 		packetType1RTT, debugFrameConnectionCloseTransport{
 			code: errNo,
 		})
 
-	tc.advance(1100 * time.Microsecond)
+	time.Sleep(1100 * time.Microsecond)
 	tc.writeFrames(packetType1RTT, debugFramePing{})
 	tc.wantIdle("no response to packet, because CONN_CLOSE backoff is now 2ms")
 
-	tc.advance(1000 * time.Microsecond)
+	time.Sleep(1000 * time.Microsecond)
 	tc.writeFrames(packetType1RTT, debugFramePing{})
 	tc.wantFrame("2ms since last CONN_CLOSE, receiving a packet generates another CONN_CLOSE",
 		packetType1RTT, debugFrameConnectionCloseTransport{
@@ -55,7 +61,7 @@
 		t.Errorf("conn.Wait() = %v, want still waiting", err)
 	}
 
-	tc.advance(100000 * time.Microsecond)
+	time.Sleep(100000 * time.Microsecond)
 	tc.writeFrames(packetType1RTT, debugFramePing{})
 	tc.wantIdle("drain timer expired, no more responses")
 
@@ -68,6 +74,9 @@
 }
 
 func TestConnCloseWithPeerResponse(t *testing.T) {
+	synctest.Test(t, testConnCloseWithPeerResponse)
+}
+func testConnCloseWithPeerResponse(t *testing.T) {
 	qr := &qlogRecord{}
 	tc := newTestConn(t, clientSide, qr.config)
 	tc.handshake()
@@ -99,7 +108,7 @@
 		t.Errorf("non-blocking conn.Wait() = %v, want %v", err, wantErr)
 	}
 
-	tc.advance(1 * time.Second) // long enough to exit the draining state
+	time.Sleep(1 * time.Second) // long enough to exit the draining state
 	qr.wantEvents(t, jsonEvent{
 		"name": "connectivity:connection_closed",
 		"data": map[string]any{
@@ -109,6 +118,9 @@
 }
 
 func TestConnClosePeerCloses(t *testing.T) {
+	synctest.Test(t, testConnClosePeerCloses)
+}
+func testConnClosePeerCloses(t *testing.T) {
 	qr := &qlogRecord{}
 	tc := newTestConn(t, clientSide, qr.config)
 	tc.handshake()
@@ -137,7 +149,7 @@
 			reason: "because",
 		})
 
-	tc.advance(1 * time.Second) // long enough to exit the draining state
+	time.Sleep(1 * time.Second) // long enough to exit the draining state
 	qr.wantEvents(t, jsonEvent{
 		"name": "connectivity:connection_closed",
 		"data": map[string]any{
@@ -147,6 +159,9 @@
 }
 
 func TestConnCloseReceiveInInitial(t *testing.T) {
+	synctest.Test(t, testConnCloseReceiveInInitial)
+}
+func testConnCloseReceiveInInitial(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	tc.wantFrame("client sends Initial CRYPTO frame",
 		packetTypeInitial, debugFrameCrypto{
@@ -171,6 +186,9 @@
 }
 
 func TestConnCloseReceiveInHandshake(t *testing.T) {
+	synctest.Test(t, testConnCloseReceiveInHandshake)
+}
+func testConnCloseReceiveInHandshake(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	tc.ignoreFrame(frameTypeAck)
 	tc.wantFrame("client sends Initial CRYPTO frame",
@@ -204,6 +222,9 @@
 }
 
 func TestConnCloseClosedByEndpoint(t *testing.T) {
+	synctest.Test(t, testConnCloseClosedByEndpoint)
+}
+func testConnCloseClosedByEndpoint(t *testing.T) {
 	ctx := canceledContext()
 	tc := newTestConn(t, clientSide)
 	tc.handshake()
@@ -231,6 +252,9 @@
 }
 
 func TestConnCloseUnblocksAcceptStream(t *testing.T) {
+	synctest.Test(t, testConnCloseUnblocksAcceptStream)
+}
+func testConnCloseUnblocksAcceptStream(t *testing.T) {
 	testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error {
 		_, err := tc.conn.AcceptStream(ctx)
 		return err
@@ -238,6 +262,9 @@
 }
 
 func TestConnCloseUnblocksNewStream(t *testing.T) {
+	synctest.Test(t, testConnCloseUnblocksNewStream)
+}
+func testConnCloseUnblocksNewStream(t *testing.T) {
 	testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error {
 		_, err := tc.conn.NewStream(ctx)
 		return err
@@ -245,6 +272,9 @@
 }
 
 func TestConnCloseUnblocksStreamRead(t *testing.T) {
+	synctest.Test(t, testConnCloseUnblocksStreamRead)
+}
+func testConnCloseUnblocksStreamRead(t *testing.T) {
 	testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error {
 		s := newLocalStream(t, tc, bidiStream)
 		s.SetReadContext(ctx)
@@ -255,6 +285,9 @@
 }
 
 func TestConnCloseUnblocksStreamWrite(t *testing.T) {
+	synctest.Test(t, testConnCloseUnblocksStreamWrite)
+}
+func testConnCloseUnblocksStreamWrite(t *testing.T) {
 	testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error {
 		s := newLocalStream(t, tc, bidiStream)
 		s.SetWriteContext(ctx)
@@ -267,6 +300,9 @@
 }
 
 func TestConnCloseUnblocksStreamClose(t *testing.T) {
+	synctest.Test(t, testConnCloseUnblocksStreamClose)
+}
+func testConnCloseUnblocksStreamClose(t *testing.T) {
 	testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error {
 		s := newLocalStream(t, tc, bidiStream)
 		s.SetWriteContext(ctx)
diff --git a/quic/conn_flow_test.go b/quic/conn_flow_test.go
index 52ecf92..d8d3ae7 100644
--- a/quic/conn_flow_test.go
+++ b/quic/conn_flow_test.go
@@ -2,14 +2,20 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
 	"context"
 	"testing"
+	"testing/synctest"
 )
 
 func TestConnInflowReturnOnRead(t *testing.T) {
+	synctest.Test(t, testConnInflowReturnOnRead)
+}
+func testConnInflowReturnOnRead(t *testing.T) {
 	tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) {
 		c.MaxConnReadBufferSize = 64
 	})
@@ -41,6 +47,9 @@
 }
 
 func TestConnInflowReturnOnRacingReads(t *testing.T) {
+	synctest.Test(t, testConnInflowReturnOnRacingReads)
+}
+func testConnInflowReturnOnRacingReads(t *testing.T) {
 	// Perform two reads at the same time,
 	// one for half of MaxConnReadBufferSize
 	// and one for one byte.
@@ -91,6 +100,9 @@
 }
 
 func TestConnInflowReturnOnClose(t *testing.T) {
+	synctest.Test(t, testConnInflowReturnOnClose)
+}
+func testConnInflowReturnOnClose(t *testing.T) {
 	tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) {
 		c.MaxConnReadBufferSize = 64
 	})
@@ -107,6 +119,9 @@
 }
 
 func TestConnInflowReturnOnReset(t *testing.T) {
+	synctest.Test(t, testConnInflowReturnOnReset)
+}
+func testConnInflowReturnOnReset(t *testing.T) {
 	tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) {
 		c.MaxConnReadBufferSize = 64
 	})
@@ -127,6 +142,9 @@
 }
 
 func TestConnInflowStreamViolation(t *testing.T) {
+	synctest.Test(t, testConnInflowStreamViolation)
+}
+func testConnInflowStreamViolation(t *testing.T) {
 	tc := newTestConn(t, serverSide, func(c *Config) {
 		c.MaxConnReadBufferSize = 100
 	})
@@ -169,6 +187,9 @@
 }
 
 func TestConnInflowResetViolation(t *testing.T) {
+	synctest.Test(t, testConnInflowResetViolation)
+}
+func testConnInflowResetViolation(t *testing.T) {
 	tc := newTestConn(t, serverSide, func(c *Config) {
 		c.MaxConnReadBufferSize = 100
 	})
@@ -197,6 +218,9 @@
 }
 
 func TestConnInflowMultipleStreams(t *testing.T) {
+	synctest.Test(t, testConnInflowMultipleStreams)
+}
+func testConnInflowMultipleStreams(t *testing.T) {
 	tc := newTestConn(t, serverSide, func(c *Config) {
 		c.MaxConnReadBufferSize = 128
 	})
@@ -247,6 +271,9 @@
 }
 
 func TestConnOutflowBlocked(t *testing.T) {
+	synctest.Test(t, testConnOutflowBlocked)
+}
+func testConnOutflowBlocked(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, clientSide, uniStream,
 		permissiveTransportParameters,
 		func(p *transportParameters) {
@@ -291,6 +318,9 @@
 }
 
 func TestConnOutflowMaxDataDecreases(t *testing.T) {
+	synctest.Test(t, testConnOutflowMaxDataDecreases)
+}
+func testConnOutflowMaxDataDecreases(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, clientSide, uniStream,
 		permissiveTransportParameters,
 		func(p *transportParameters) {
@@ -318,6 +348,9 @@
 }
 
 func TestConnOutflowMaxDataRoundRobin(t *testing.T) {
+	synctest.Test(t, testConnOutflowMaxDataRoundRobin)
+}
+func testConnOutflowMaxDataRoundRobin(t *testing.T) {
 	ctx := canceledContext()
 	tc := newTestConn(t, clientSide, permissiveTransportParameters,
 		func(p *transportParameters) {
@@ -370,6 +403,9 @@
 }
 
 func TestConnOutflowMetaAndData(t *testing.T) {
+	synctest.Test(t, testConnOutflowMetaAndData)
+}
+func testConnOutflowMetaAndData(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, clientSide, bidiStream,
 		permissiveTransportParameters,
 		func(p *transportParameters) {
@@ -398,6 +434,9 @@
 }
 
 func TestConnOutflowResentData(t *testing.T) {
+	synctest.Test(t, testConnOutflowResentData)
+}
+func testConnOutflowResentData(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, clientSide, bidiStream,
 		permissiveTransportParameters,
 		func(p *transportParameters) {
diff --git a/quic/conn_id_test.go b/quic/conn_id_test.go
index c9da0eb..4b4da67 100644
--- a/quic/conn_id_test.go
+++ b/quic/conn_id_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -11,9 +13,13 @@
 	"net/netip"
 	"strings"
 	"testing"
+	"testing/synctest"
 )
 
 func TestConnIDClientHandshake(t *testing.T) {
+	synctest.Test(t, testConnIDClientHandshake)
+}
+func testConnIDClientHandshake(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	// On initialization, the client chooses local and remote IDs.
 	//
@@ -57,6 +63,9 @@
 }
 
 func TestConnIDServerHandshake(t *testing.T) {
+	synctest.Test(t, testConnIDServerHandshake)
+}
+func testConnIDServerHandshake(t *testing.T) {
 	tc := newTestConn(t, serverSide)
 	// On initialization, the server is provided with the client-chosen
 	// transient connection ID, and allocates an ID of its own.
@@ -178,6 +187,9 @@
 }
 
 func TestConnIDPeerRequestsManyIDs(t *testing.T) {
+	synctest.Test(t, testConnIDPeerRequestsManyIDs)
+}
+func testConnIDPeerRequestsManyIDs(t *testing.T) {
 	// "An endpoint SHOULD ensure that its peer has a sufficient number
 	// of available and unused connection IDs."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-4
@@ -220,6 +232,9 @@
 }
 
 func TestConnIDPeerProvidesTooManyIDs(t *testing.T) {
+	synctest.Test(t, testConnIDPeerProvidesTooManyIDs)
+}
+func testConnIDPeerProvidesTooManyIDs(t *testing.T) {
 	// "An endpoint MUST NOT provide more connection IDs than the peer's limit."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-4
 	tc := newTestConn(t, serverSide)
@@ -238,6 +253,9 @@
 }
 
 func TestConnIDPeerTemporarilyExceedsActiveConnIDLimit(t *testing.T) {
+	synctest.Test(t, testConnIDPeerTemporarilyExceedsActiveConnIDLimit)
+}
+func testConnIDPeerTemporarilyExceedsActiveConnIDLimit(t *testing.T) {
 	// "An endpoint MAY send connection IDs that temporarily exceed a peer's limit
 	// if the NEW_CONNECTION_ID frame also requires the retirement of any excess [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-4
@@ -272,7 +290,7 @@
 		clientSide,
 		serverSide,
 	} {
-		t.Run(side.String(), func(t *testing.T) {
+		synctestSubtest(t, side.String(), func(t *testing.T) {
 			tc := newTestConn(t, side)
 			tc.handshake()
 			tc.ignoreFrame(frameTypeAck)
@@ -293,6 +311,9 @@
 }
 
 func TestConnIDPeerWithZeroLengthConnIDSendsNewConnectionID(t *testing.T) {
+	synctest.Test(t, testConnIDPeerWithZeroLengthConnIDSendsNewConnectionID)
+}
+func testConnIDPeerWithZeroLengthConnIDSendsNewConnectionID(t *testing.T) {
 	// "An endpoint that selects a zero-length connection ID during the handshake
 	// cannot issue a new connection ID."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-8
@@ -315,6 +336,9 @@
 }
 
 func TestConnIDPeerRequestsRetirement(t *testing.T) {
+	synctest.Test(t, testConnIDPeerRequestsRetirement)
+}
+func testConnIDPeerRequestsRetirement(t *testing.T) {
 	// "Upon receipt of an increased Retire Prior To field, the peer MUST
 	// stop using the corresponding connection IDs and retire them with
 	// RETIRE_CONNECTION_ID frames [...]"
@@ -339,6 +363,9 @@
 }
 
 func TestConnIDPeerDoesNotAcknowledgeRetirement(t *testing.T) {
+	synctest.Test(t, testConnIDPeerDoesNotAcknowledgeRetirement)
+}
+func testConnIDPeerDoesNotAcknowledgeRetirement(t *testing.T) {
 	// "An endpoint SHOULD limit the number of connection IDs it has retired locally
 	// for which RETIRE_CONNECTION_ID frames have not yet been acknowledged."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-5.1.2-6
@@ -364,6 +391,9 @@
 }
 
 func TestConnIDRepeatedNewConnectionIDFrame(t *testing.T) {
+	synctest.Test(t, testConnIDRepeatedNewConnectionIDFrame)
+}
+func testConnIDRepeatedNewConnectionIDFrame(t *testing.T) {
 	// "Receipt of the same [NEW_CONNECTION_ID] frame multiple times
 	// MUST NOT be treated as a connection error.
 	// https://www.rfc-editor.org/rfc/rfc9000#section-19.15-7
@@ -387,6 +417,9 @@
 }
 
 func TestConnIDForSequenceNumberChanges(t *testing.T) {
+	synctest.Test(t, testConnIDForSequenceNumberChanges)
+}
+func testConnIDForSequenceNumberChanges(t *testing.T) {
 	// "[...] if a sequence number is used for different connection IDs,
 	// the endpoint MAY treat that receipt as a connection error
 	// of type PROTOCOL_VIOLATION."
@@ -415,6 +448,9 @@
 }
 
 func TestConnIDRetirePriorToAfterNewConnID(t *testing.T) {
+	synctest.Test(t, testConnIDRetirePriorToAfterNewConnID)
+}
+func testConnIDRetirePriorToAfterNewConnID(t *testing.T) {
 	// "Receiving a value in the Retire Prior To field that is greater than
 	// that in the Sequence Number field MUST be treated as a connection error
 	// of type FRAME_ENCODING_ERROR.
@@ -436,6 +472,9 @@
 }
 
 func TestConnIDAlreadyRetired(t *testing.T) {
+	synctest.Test(t, testConnIDAlreadyRetired)
+}
+func testConnIDAlreadyRetired(t *testing.T) {
 	// "An endpoint that receives a NEW_CONNECTION_ID frame with a
 	// sequence number smaller than the Retire Prior To field of a
 	// previously received NEW_CONNECTION_ID frame MUST send a
@@ -472,6 +511,9 @@
 }
 
 func TestConnIDRepeatedRetireConnectionIDFrame(t *testing.T) {
+	synctest.Test(t, testConnIDRepeatedRetireConnectionIDFrame)
+}
+func testConnIDRepeatedRetireConnectionIDFrame(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	tc.handshake()
 	tc.ignoreFrame(frameTypeAck)
@@ -493,6 +535,9 @@
 }
 
 func TestConnIDRetiredUnsent(t *testing.T) {
+	synctest.Test(t, testConnIDRetiredUnsent)
+}
+func testConnIDRetiredUnsent(t *testing.T) {
 	// "Receipt of a RETIRE_CONNECTION_ID frame containing a sequence number
 	// greater than any previously sent to the peer MUST be treated as a
 	// connection error of type PROTOCOL_VIOLATION."
@@ -512,6 +557,9 @@
 }
 
 func TestConnIDUsePreferredAddressConnID(t *testing.T) {
+	synctest.Test(t, testConnIDUsePreferredAddressConnID)
+}
+func testConnIDUsePreferredAddressConnID(t *testing.T) {
 	// Peer gives us a connection ID in the preferred address transport parameter.
 	// We don't use the preferred address at this time, but we should use the
 	// connection ID. (It isn't tied to any specific address.)
@@ -543,6 +591,9 @@
 }
 
 func TestConnIDPeerProvidesPreferredAddrAndTooManyConnIDs(t *testing.T) {
+	synctest.Test(t, testConnIDPeerProvidesPreferredAddrAndTooManyConnIDs)
+}
+func testConnIDPeerProvidesPreferredAddrAndTooManyConnIDs(t *testing.T) {
 	// Peer gives us more conn ids than our advertised limit,
 	// including a conn id in the preferred address transport parameter.
 	cid := testPeerConnID(10)
@@ -568,6 +619,9 @@
 }
 
 func TestConnIDPeerWithZeroLengthIDProvidesPreferredAddr(t *testing.T) {
+	synctest.Test(t, testConnIDPeerWithZeroLengthIDProvidesPreferredAddr)
+}
+func testConnIDPeerWithZeroLengthIDProvidesPreferredAddr(t *testing.T) {
 	// Peer gives us more conn ids than our advertised limit,
 	// including a conn id in the preferred address transport parameter.
 	tc := newTestConn(t, serverSide, func(p *transportParameters) {
@@ -596,7 +650,7 @@
 	// "Endpoints MUST validate that received [initial_source_connection_id]
 	// parameters match received connection ID values."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-7.3-3
-	testSides(t, "", func(t *testing.T, side connSide) {
+	testSidesSynctest(t, "", func(t *testing.T, side connSide) {
 		tc := newTestConn(t, side, func(p *transportParameters) {
 			p.initialSrcConnID = []byte("invalid")
 		})
@@ -621,7 +675,7 @@
 }
 
 func TestConnIDsCleanedUpAfterClose(t *testing.T) {
-	testSides(t, "", func(t *testing.T, side connSide) {
+	testSidesSynctest(t, "", func(t *testing.T, side connSide) {
 		tc := newTestConn(t, side, func(p *transportParameters) {
 			if side == clientSide {
 				token := testPeerStatelessResetToken(0)
@@ -664,6 +718,9 @@
 }
 
 func TestConnIDRetiredConnIDResent(t *testing.T) {
+	synctest.Test(t, testConnIDRetiredConnIDResent)
+}
+func testConnIDRetiredConnIDResent(t *testing.T) {
 	tc := newTestConn(t, serverSide)
 	tc.handshake()
 	tc.ignoreFrame(frameTypeAck)
diff --git a/quic/conn_loss_test.go b/quic/conn_loss_test.go
index f13ea13..49c794f 100644
--- a/quic/conn_loss_test.go
+++ b/quic/conn_loss_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -9,6 +11,8 @@
 	"crypto/tls"
 	"fmt"
 	"testing"
+	"testing/synctest"
+	"time"
 )
 
 // Frames may be retransmitted either when the packet containing the frame is lost, or on PTO.
@@ -22,6 +26,16 @@
 	})
 }
 
+func lostFrameTestSynctest(t *testing.T, f func(t *testing.T, pto bool)) {
+	t.Helper()
+	lostFrameTest(t, func(t *testing.T, pto bool) {
+		t.Helper()
+		synctest.Test(t, func(t *testing.T) {
+			f(t, pto)
+		})
+	})
+}
+
 // triggerLossOrPTO causes the conn to declare the last sent packet lost,
 // or advances to the PTO timer.
 func (tc *testConn) triggerLossOrPTO(ptype packetType, pto bool) {
@@ -33,7 +47,11 @@
 		if *testVV {
 			tc.t.Logf("advancing to PTO timer")
 		}
-		tc.advanceTo(tc.conn.loss.timer)
+		var when time.Time
+		tc.conn.runOnLoop(tc.t.Context(), func(now time.Time, conn *Conn) {
+			when = conn.loss.timer
+		})
+		time.Sleep(time.Until(when))
 		return
 	}
 	if *testVV {
@@ -77,7 +95,7 @@
 	// "Cancellation of stream transmission, as carried in a RESET_STREAM frame,
 	// is sent until acknowledged or until all stream data is acknowledged by the peer [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.4
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters)
 		tc.ignoreFrame(frameTypeAck)
 
@@ -106,7 +124,7 @@
 	// Technically, we can stop sending a STOP_SENDING frame if the peer sends
 	// us all the data for the stream or resets it. We don't bother tracking this,
 	// however, so we'll keep sending the frame until it is acked. This is harmless.
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, permissiveTransportParameters)
 		tc.ignoreFrame(frameTypeAck)
 
@@ -127,7 +145,7 @@
 func TestLostCryptoFrame(t *testing.T) {
 	// "Data sent in CRYPTO frames is retransmitted [...] until all data has been acknowledged."
 	// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.1
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		tc := newTestConn(t, clientSide)
 		tc.ignoreFrame(frameTypeAck)
 
@@ -171,7 +189,7 @@
 func TestLostStreamFrameEmpty(t *testing.T) {
 	// A STREAM frame opening a stream, but containing no stream data, should
 	// be retransmitted if lost.
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		ctx := canceledContext()
 		tc := newTestConn(t, clientSide, permissiveTransportParameters)
 		tc.handshake()
@@ -203,7 +221,7 @@
 	// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.2
 	//
 	// TODO: Lost stream frame after RESET_STREAM
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		data := []byte{0, 1, 2, 3, 4, 5, 6, 7}
 		tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) {
 			p.initialMaxStreamsUni = 1
@@ -247,6 +265,9 @@
 }
 
 func TestLostStreamPartialLoss(t *testing.T) {
+	synctest.Test(t, testLostStreamPartialLoss)
+}
+func testLostStreamPartialLoss(t *testing.T) {
 	// Conn sends four STREAM packets.
 	// ACKs are received for the packets containing bytes 0 and 2.
 	// The remaining packets are declared lost.
@@ -295,7 +316,7 @@
 	// "An updated value is sent in a MAX_DATA frame if the packet
 	// containing the most recently sent MAX_DATA frame is declared lost [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.7
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		const maxWindowSize = 32
 		buf := make([]byte, maxWindowSize)
 		tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) {
@@ -340,7 +361,7 @@
 	// "[...] an updated value is sent when the packet containing
 	// the most recent MAX_STREAM_DATA frame for a stream is lost"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.8
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		const maxWindowSize = 32
 		buf := make([]byte, maxWindowSize)
 		tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) {
@@ -387,7 +408,7 @@
 	// "An endpoint SHOULD stop sending MAX_STREAM_DATA frames when
 	// the receiving part of the stream enters a "Size Known" or "Reset Recvd" state."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.8
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		const maxWindowSize = 10
 		buf := make([]byte, maxWindowSize)
 		tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) {
@@ -425,7 +446,7 @@
 	// most recent MAX_STREAMS for a stream type frame is declared lost [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.9
 	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
-		lostFrameTest(t, func(t *testing.T, pto bool) {
+		lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 			ctx := canceledContext()
 			tc := newTestConn(t, serverSide, func(c *Config) {
 				c.MaxUniRemoteStreams = 1
@@ -469,6 +490,9 @@
 }
 
 func TestLostMaxStreamsFrameNotMostRecent(t *testing.T) {
+	synctest.Test(t, testLostMaxStreamsFrameNotMostRecent)
+}
+func testLostMaxStreamsFrameNotMostRecent(t *testing.T) {
 	// Send two MAX_STREAMS frames, lose the first one.
 	//
 	// No PTO mode for this test: The ack that causes the first frame
@@ -514,7 +538,7 @@
 	// "A new [STREAM_DATA_BLOCKED] frame is sent if a packet containing
 	// the most recent frame for a scope is lost [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.10
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) {
 			p.initialMaxStreamsUni = 1
 			p.initialMaxData = 1 << 20
@@ -565,7 +589,7 @@
 	// "A new [STREAM_DATA_BLOCKED] frame is sent [...] only while
 	// the endpoint is blocked on the corresponding limit."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.10
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) {
 			p.initialMaxStreamsUni = 1
 			p.initialMaxData = 1 << 20
@@ -607,7 +631,7 @@
 func TestLostNewConnectionIDFrame(t *testing.T) {
 	// "New connection IDs are [...] retransmitted if the packet containing them is lost."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.13
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		tc := newTestConn(t, serverSide)
 		tc.handshake()
 		tc.ignoreFrame(frameTypeAck)
@@ -637,7 +661,7 @@
 	// "[...] retired connection IDs are [...] retransmitted
 	// if the packet containing them is lost."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.13
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		tc := newTestConn(t, clientSide)
 		tc.handshake()
 		tc.ignoreFrame(frameTypeAck)
@@ -664,7 +688,7 @@
 func TestLostPathResponseFrame(t *testing.T) {
 	// "Responses to path validation using PATH_RESPONSE frames are sent just once."
 	// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.12
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		tc := newTestConn(t, clientSide)
 		tc.handshake()
 		tc.ignoreFrame(frameTypeAck)
@@ -687,7 +711,7 @@
 func TestLostHandshakeDoneFrame(t *testing.T) {
 	// "The HANDSHAKE_DONE frame MUST be retransmitted until it is acknowledged."
 	// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.16
-	lostFrameTest(t, func(t *testing.T, pto bool) {
+	lostFrameTestSynctest(t, func(t *testing.T, pto bool) {
 		tc := newTestConn(t, serverSide)
 		tc.ignoreFrame(frameTypeAck)
 
diff --git a/quic/conn_recv_test.go b/quic/conn_recv_test.go
index 1a0eb3a..6ee728e 100644
--- a/quic/conn_recv_test.go
+++ b/quic/conn_recv_test.go
@@ -2,14 +2,20 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
 	"crypto/tls"
 	"testing"
+	"testing/synctest"
 )
 
 func TestConnReceiveAckForUnsentPacket(t *testing.T) {
+	synctest.Test(t, testConnReceiveAckForUnsentPacket)
+}
+func testConnReceiveAckForUnsentPacket(t *testing.T) {
 	tc := newTestConn(t, serverSide, permissiveTransportParameters)
 	tc.handshake()
 	tc.writeFrames(packetType1RTT,
@@ -27,6 +33,9 @@
 // drop state for a number space, and also contains a valid ACK frame for that space,
 // we shouldn't complain about the ACK.
 func TestConnReceiveAckForDroppedSpace(t *testing.T) {
+	synctest.Test(t, testConnReceiveAckForDroppedSpace)
+}
+func testConnReceiveAckForDroppedSpace(t *testing.T) {
 	tc := newTestConn(t, serverSide, permissiveTransportParameters)
 	tc.ignoreFrame(frameTypeAck)
 	tc.ignoreFrame(frameTypeNewConnectionID)
diff --git a/quic/conn_send_test.go b/quic/conn_send_test.go
index c5cf936..88911bd 100644
--- a/quic/conn_send_test.go
+++ b/quic/conn_send_test.go
@@ -2,14 +2,20 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
 	"testing"
+	"testing/synctest"
 	"time"
 )
 
 func TestAckElicitingAck(t *testing.T) {
+	synctest.Test(t, testAckElicitingAck)
+}
+func testAckElicitingAck(t *testing.T) {
 	// "A receiver that sends only non-ack-eliciting packets [...] might not receive
 	// an acknowledgment for a long period of time.
 	// [...] a receiver could send a [...] ack-eliciting frame occasionally [...]
@@ -22,7 +28,7 @@
 	tc.handshake()
 	const count = 100
 	for i := 0; i < count; i++ {
-		tc.advance(1 * time.Millisecond)
+		time.Sleep(1 * time.Millisecond)
 		tc.writeFrames(packetType1RTT,
 			debugFramePing{},
 		)
@@ -38,6 +44,9 @@
 }
 
 func TestSendPacketNumberSize(t *testing.T) {
+	synctest.Test(t, testSendPacketNumberSize)
+}
+func testSendPacketNumberSize(t *testing.T) {
 	tc := newTestConn(t, clientSide, permissiveTransportParameters)
 	tc.handshake()
 
diff --git a/quic/conn_streams.go b/quic/conn_streams.go
index 80884fd..0e4bf50 100644
--- a/quic/conn_streams.go
+++ b/quic/conn_streams.go
@@ -71,7 +71,7 @@
 
 // AcceptStream waits for and returns the next stream created by the peer.
 func (c *Conn) AcceptStream(ctx context.Context) (*Stream, error) {
-	return c.streams.queue.get(ctx, c.testHooks)
+	return c.streams.queue.get(ctx)
 }
 
 // NewStream creates a stream.
diff --git a/quic/conn_streams_test.go b/quic/conn_streams_test.go
index af3c1de..b95aa47 100644
--- a/quic/conn_streams_test.go
+++ b/quic/conn_streams_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -11,9 +13,13 @@
 	"math"
 	"sync"
 	"testing"
+	"testing/synctest"
 )
 
 func TestStreamsCreate(t *testing.T) {
+	synctest.Test(t, testStreamsCreate)
+}
+func testStreamsCreate(t *testing.T) {
 	ctx := canceledContext()
 	tc := newTestConn(t, clientSide, permissiveTransportParameters)
 	tc.handshake()
@@ -53,6 +59,9 @@
 }
 
 func TestStreamsAccept(t *testing.T) {
+	synctest.Test(t, testStreamsAccept)
+}
+func testStreamsAccept(t *testing.T) {
 	ctx := canceledContext()
 	tc := newTestConn(t, serverSide)
 	tc.handshake()
@@ -95,6 +104,9 @@
 }
 
 func TestStreamsBlockingAccept(t *testing.T) {
+	synctest.Test(t, testStreamsBlockingAccept)
+}
+func testStreamsBlockingAccept(t *testing.T) {
 	tc := newTestConn(t, serverSide)
 	tc.handshake()
 
@@ -124,6 +136,9 @@
 }
 
 func TestStreamsLocalStreamNotCreated(t *testing.T) {
+	synctest.Test(t, testStreamsLocalStreamNotCreated)
+}
+func testStreamsLocalStreamNotCreated(t *testing.T) {
 	// "An endpoint MUST terminate the connection with error STREAM_STATE_ERROR
 	// if it receives a STREAM frame for a locally initiated stream that has
 	// not yet been created [...]"
@@ -142,6 +157,9 @@
 }
 
 func TestStreamsLocalStreamClosed(t *testing.T) {
+	synctest.Test(t, testStreamsLocalStreamClosed)
+}
+func testStreamsLocalStreamClosed(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, clientSide, uniStream, permissiveTransportParameters)
 	s.CloseWrite()
 	tc.wantFrame("FIN for closed stream",
@@ -168,6 +186,9 @@
 }
 
 func TestStreamsStreamSendOnly(t *testing.T) {
+	synctest.Test(t, testStreamsStreamSendOnly)
+}
+func testStreamsStreamSendOnly(t *testing.T) {
 	// "An endpoint MUST terminate the connection with error STREAM_STATE_ERROR
 	// if it receives a STREAM frame for a locally initiated stream that has
 	// not yet been created [...]"
@@ -198,6 +219,9 @@
 }
 
 func TestStreamsWriteQueueFairness(t *testing.T) {
+	synctest.Test(t, testStreamsWriteQueueFairness)
+}
+func testStreamsWriteQueueFairness(t *testing.T) {
 	ctx := canceledContext()
 	const dataLen = 1 << 20
 	const numStreams = 3
@@ -233,7 +257,7 @@
 		}
 		// Wait for the stream to finish writing whatever frames it can before
 		// congestion control blocks it.
-		tc.wait()
+		synctest.Wait()
 	}
 
 	sent := make([]int64, len(streams))
@@ -344,7 +368,7 @@
 		},
 	}} {
 		name := fmt.Sprintf("%v/%v/%v", test.side, test.styp, test.name)
-		t.Run(name, func(t *testing.T) {
+		synctestSubtest(t, name, func(t *testing.T) {
 			tc, s := newTestConnAndStream(t, serverSide, test.side, test.styp,
 				permissiveTransportParameters)
 			tc.ignoreFrame(frameTypeStreamBase)
@@ -364,6 +388,9 @@
 }
 
 func TestStreamsCreateAndCloseRemote(t *testing.T) {
+	synctest.Test(t, testStreamsCreateAndCloseRemote)
+}
+func testStreamsCreateAndCloseRemote(t *testing.T) {
 	// This test exercises creating new streams in response to frames
 	// from the peer, and cleaning up after streams are fully closed.
 	//
@@ -473,6 +500,9 @@
 }
 
 func TestStreamsCreateConcurrency(t *testing.T) {
+	synctest.Test(t, testStreamsCreateConcurrency)
+}
+func testStreamsCreateConcurrency(t *testing.T) {
 	cli, srv := newLocalConnPair(t, &Config{}, &Config{})
 
 	srvdone := make(chan int)
@@ -520,6 +550,9 @@
 }
 
 func TestStreamsPTOWithImplicitStream(t *testing.T) {
+	synctest.Test(t, testStreamsPTOWithImplicitStream)
+}
+func testStreamsPTOWithImplicitStream(t *testing.T) {
 	ctx := canceledContext()
 	tc := newTestConn(t, serverSide, permissiveTransportParameters)
 	tc.handshake()
diff --git a/quic/conn_test.go b/quic/conn_test.go
index a5f2f61..81eeffc 100644
--- a/quic/conn_test.go
+++ b/quic/conn_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -17,6 +19,7 @@
 	"reflect"
 	"strings"
 	"testing"
+	"testing/synctest"
 	"time"
 
 	"golang.org/x/net/quic/qlog"
@@ -27,7 +30,8 @@
 	qlogdir = flag.String("qlog", "", "write qlog logs to directory")
 )
 
-func TestConnTestConn(t *testing.T) {
+func TestConnTestConn(t *testing.T) { synctest.Test(t, testConnTestConn) }
+func testConnTestConn(t *testing.T) {
 	tc := newTestConn(t, serverSide)
 	tc.handshake()
 	if got, want := tc.timeUntilEvent(), defaultMaxIdleTimeout; got != want {
@@ -40,13 +44,13 @@
 		})
 		return
 	}).result()
-	if !ranAt.Equal(tc.endpoint.now) {
-		t.Errorf("func ran on loop at %v, want %v", ranAt, tc.endpoint.now)
+	if !ranAt.Equal(time.Now()) {
+		t.Errorf("func ran on loop at %v, want %v", ranAt, time.Now())
 	}
-	tc.wait()
+	synctest.Wait()
 
-	nextTime := tc.endpoint.now.Add(defaultMaxIdleTimeout / 2)
-	tc.advanceTo(nextTime)
+	nextTime := time.Now().Add(defaultMaxIdleTimeout / 2)
+	time.Sleep(time.Until(nextTime))
 	ranAt, _ = runAsync(tc, func(ctx context.Context) (when time.Time, _ error) {
 		tc.conn.runOnLoop(ctx, func(now time.Time, c *Conn) {
 			when = now
@@ -56,7 +60,7 @@
 	if !ranAt.Equal(nextTime) {
 		t.Errorf("func ran on loop at %v, want %v", ranAt, nextTime)
 	}
-	tc.wait()
+	synctest.Wait()
 
 	tc.advanceToTimer()
 	if got := tc.conn.lifetime.state; got != connStateDone {
@@ -125,12 +129,9 @@
 // A testConn is a Conn whose external interactions (sending and receiving packets,
 // setting timers) can be manipulated in tests.
 type testConn struct {
-	t              *testing.T
-	conn           *Conn
-	endpoint       *testEndpoint
-	timer          time.Time
-	timerLastFired time.Time
-	idlec          chan struct{} // only accessed on the conn's loop
+	t        *testing.T
+	conn     *Conn
+	endpoint *testEndpoint
 
 	// Keys are distinct from the conn's keys,
 	// because the test may know about keys before the conn does.
@@ -183,8 +184,6 @@
 	// Values to set in packets sent to the conn.
 	sendKeyNumber   int
 	sendKeyPhaseBit bool
-
-	asyncTestState
 }
 
 type test1RTTKeys struct {
@@ -198,10 +197,6 @@
 }
 
 // newTestConn creates a Conn for testing.
-//
-// The Conn's event loop is controlled by the test,
-// allowing test code to access Conn state directly
-// by first ensuring the loop goroutine is idle.
 func newTestConn(t *testing.T, side connSide, opts ...any) *testConn {
 	t.Helper()
 	config := &Config{
@@ -242,7 +237,7 @@
 	endpoint.configTransportParams = configTransportParams
 	endpoint.configTestConn = configTestConn
 	conn, err := endpoint.e.newConn(
-		endpoint.now,
+		time.Now(),
 		config,
 		side,
 		cids,
@@ -252,7 +247,7 @@
 		t.Fatal(err)
 	}
 	tc := endpoint.conns[conn]
-	tc.wait()
+	synctest.Wait()
 	return tc
 }
 
@@ -306,76 +301,33 @@
 	return tc
 }
 
-// advance causes time to pass.
-func (tc *testConn) advance(d time.Duration) {
-	tc.t.Helper()
-	tc.endpoint.advance(d)
-}
-
-// advanceTo sets the current time.
-func (tc *testConn) advanceTo(now time.Time) {
-	tc.t.Helper()
-	tc.endpoint.advanceTo(now)
-}
-
 // advanceToTimer sets the current time to the time of the Conn's next timer event.
 func (tc *testConn) advanceToTimer() {
-	if tc.timer.IsZero() {
+	when := tc.nextEvent()
+	if when.IsZero() {
 		tc.t.Fatalf("advancing to timer, but timer is not set")
 	}
-	tc.advanceTo(tc.timer)
-}
-
-func (tc *testConn) timerDelay() time.Duration {
-	if tc.timer.IsZero() {
-		return math.MaxInt64 // infinite
-	}
-	if tc.timer.Before(tc.endpoint.now) {
-		return 0
-	}
-	return tc.timer.Sub(tc.endpoint.now)
+	time.Sleep(time.Until(when))
+	synctest.Wait()
 }
 
 const infiniteDuration = time.Duration(math.MaxInt64)
 
 // timeUntilEvent returns the amount of time until the next connection event.
 func (tc *testConn) timeUntilEvent() time.Duration {
-	if tc.timer.IsZero() {
+	next := tc.nextEvent()
+	if next.IsZero() {
 		return infiniteDuration
 	}
-	if tc.timer.Before(tc.endpoint.now) {
-		return 0
-	}
-	return tc.timer.Sub(tc.endpoint.now)
+	return max(0, time.Until(next))
 }
 
-// wait blocks until the conn becomes idle.
-// The conn is idle when it is blocked waiting for a packet to arrive or a timer to expire.
-// Tests shouldn't need to call wait directly.
-// testConn methods that wake the Conn event loop will call wait for them.
-func (tc *testConn) wait() {
-	tc.t.Helper()
-	idlec := make(chan struct{})
-	fail := false
-	tc.conn.sendMsg(func(now time.Time, c *Conn) {
-		if tc.idlec != nil {
-			tc.t.Errorf("testConn.wait called concurrently")
-			fail = true
-			close(idlec)
-		} else {
-			// nextMessage will close idlec.
-			tc.idlec = idlec
-		}
+func (tc *testConn) nextEvent() time.Time {
+	nextc := make(chan time.Time)
+	tc.conn.sendMsg(func(now, next time.Time, c *Conn) {
+		nextc <- next
 	})
-	select {
-	case <-idlec:
-	case <-tc.conn.donec:
-		// We may have async ops that can proceed now that the conn is done.
-		tc.wakeAsync()
-	}
-	if fail {
-		panic(fail)
-	}
+	return <-nextc
 }
 
 func (tc *testConn) cleanup() {
@@ -498,7 +450,7 @@
 // It returns nil if the Conn has no more datagrams to send at this time.
 func (tc *testConn) readDatagram() *testDatagram {
 	tc.t.Helper()
-	tc.wait()
+	synctest.Wait()
 	tc.sentPackets = nil
 	tc.sentFrames = nil
 	buf := tc.endpoint.read()
@@ -1103,48 +1055,10 @@
 	}
 }
 
-// nextMessage is called by the Conn's event loop to request its next event.
-func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.Time, m any) {
-	tc.timer = timer
-	for {
-		if !timer.IsZero() && !timer.After(tc.endpoint.now) {
-			if timer.Equal(tc.timerLastFired) {
-				// If the connection timer fires at time T, the Conn should take some
-				// action to advance the timer into the future. If the Conn reschedules
-				// the timer for the same time, it isn't making progress and we have a bug.
-				tc.t.Errorf("connection timer spinning; now=%v timer=%v", tc.endpoint.now, timer)
-			} else {
-				tc.timerLastFired = timer
-				return tc.endpoint.now, timerEvent{}
-			}
-		}
-		select {
-		case m := <-msgc:
-			return tc.endpoint.now, m
-		default:
-		}
-		if !tc.wakeAsync() {
-			break
-		}
-	}
-	// If the message queue is empty, then the conn is idle.
-	if tc.idlec != nil {
-		idlec := tc.idlec
-		tc.idlec = nil
-		close(idlec)
-	}
-	m = <-msgc
-	return tc.endpoint.now, m
-}
-
 func (tc *testConnHooks) newConnID(seq int64) ([]byte, error) {
 	return testLocalConnID(seq), nil
 }
 
-func (tc *testConnHooks) timeNow() time.Time {
-	return tc.endpoint.now
-}
-
 // testLocalConnID returns the connection ID with a given sequence number
 // used by a Conn under test.
 func testLocalConnID(seq int64) []byte {
diff --git a/quic/endpoint.go b/quic/endpoint.go
index 1bb9015..3d68073 100644
--- a/quic/endpoint.go
+++ b/quic/endpoint.go
@@ -36,7 +36,6 @@
 }
 
 type endpointTestHooks interface {
-	timeNow() time.Time
 	newConn(c *Conn)
 }
 
@@ -160,7 +159,7 @@
 
 // Accept waits for and returns the next connection.
 func (e *Endpoint) Accept(ctx context.Context) (*Conn, error) {
-	return e.acceptQueue.get(ctx, nil)
+	return e.acceptQueue.get(ctx)
 }
 
 // Dial creates and returns a connection to a network address.
@@ -269,12 +268,7 @@
 	if len(m.b) < minimumValidPacketSize {
 		return
 	}
-	var now time.Time
-	if e.testHooks != nil {
-		now = e.testHooks.timeNow()
-	} else {
-		now = time.Now()
-	}
+	now := time.Now()
 	// Check to see if this is a stateless reset.
 	var token statelessResetToken
 	copy(token[:], m.b[len(m.b)-len(token):])
diff --git a/quic/endpoint_test.go b/quic/endpoint_test.go
index 7ec8139..6a62104 100644
--- a/quic/endpoint_test.go
+++ b/quic/endpoint_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -12,8 +14,9 @@
 	"log/slog"
 	"net/netip"
 	"runtime"
+	"sync"
 	"testing"
-	"time"
+	"testing/synctest"
 
 	"golang.org/x/net/quic/qlog"
 )
@@ -126,22 +129,22 @@
 type testEndpoint struct {
 	t                     *testing.T
 	e                     *Endpoint
-	now                   time.Time
 	recvc                 chan *datagram
 	idlec                 chan struct{}
 	conns                 map[*Conn]*testConn
 	acceptQueue           []*testConn
 	configTransportParams []func(*transportParameters)
 	configTestConn        []func(*testConn)
-	sentDatagrams         [][]byte
 	peerTLSConn           *tls.QUICConn
 	lastInitialDstConnID  []byte // for parsing Retry packets
+
+	sentDatagramsMu sync.Mutex
+	sentDatagrams   [][]byte
 }
 
 func newTestEndpoint(t *testing.T, config *Config) *testEndpoint {
 	te := &testEndpoint{
 		t:     t,
-		now:   time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
 		recvc: make(chan *datagram),
 		idlec: make(chan struct{}),
 		conns: make(map[*Conn]*testConn),
@@ -159,16 +162,6 @@
 	te.e.Close(canceledContext())
 }
 
-func (te *testEndpoint) wait() {
-	select {
-	case te.idlec <- struct{}{}:
-	case <-te.e.closec:
-	}
-	for _, tc := range te.conns {
-		tc.wait()
-	}
-}
-
 // accept returns a server connection from the endpoint.
 // Unlike Endpoint.Accept, connections are available as soon as they are created.
 func (te *testEndpoint) accept() *testConn {
@@ -182,7 +175,7 @@
 
 func (te *testEndpoint) write(d *datagram) {
 	te.recvc <- d
-	te.wait()
+	synctest.Wait()
 }
 
 var testClientAddr = netip.MustParseAddrPort("10.0.0.1:8000")
@@ -241,7 +234,9 @@
 
 func (te *testEndpoint) read() []byte {
 	te.t.Helper()
-	te.wait()
+	synctest.Wait()
+	te.sentDatagramsMu.Lock()
+	defer te.sentDatagramsMu.Unlock()
 	if len(te.sentDatagrams) == 0 {
 		return nil
 	}
@@ -279,34 +274,9 @@
 	}
 }
 
-// advance causes time to pass.
-func (te *testEndpoint) advance(d time.Duration) {
-	te.t.Helper()
-	te.advanceTo(te.now.Add(d))
-}
-
-// advanceTo sets the current time.
-func (te *testEndpoint) advanceTo(now time.Time) {
-	te.t.Helper()
-	if te.now.After(now) {
-		te.t.Fatalf("time moved backwards: %v -> %v", te.now, now)
-	}
-	te.now = now
-	for _, tc := range te.conns {
-		if !tc.timer.After(te.now) {
-			tc.conn.sendMsg(timerEvent{})
-			tc.wait()
-		}
-	}
-}
-
 // testEndpointHooks implements endpointTestHooks.
 type testEndpointHooks testEndpoint
 
-func (te *testEndpointHooks) timeNow() time.Time {
-	return te.now
-}
-
 func (te *testEndpointHooks) newConn(c *Conn) {
 	tc := newTestConnForConn(te.t, (*testEndpoint)(te), c)
 	te.conns[c] = tc
@@ -338,6 +308,8 @@
 }
 
 func (te *testEndpointUDPConn) Write(dgram datagram) error {
+	te.sentDatagramsMu.Lock()
+	defer te.sentDatagramsMu.Unlock()
 	te.sentDatagrams = append(te.sentDatagrams, append([]byte(nil), dgram.b...))
 	return nil
 }
diff --git a/quic/gate.go b/quic/gate.go
index 1f570bb..b8b8605 100644
--- a/quic/gate.go
+++ b/quic/gate.go
@@ -46,10 +46,7 @@
 
 // waitAndLock waits until the condition is set before acquiring the gate.
 // If the context expires, waitAndLock returns an error and does not acquire the gate.
-func (g *gate) waitAndLock(ctx context.Context, testHooks connTestHooks) error {
-	if testHooks != nil {
-		return testHooks.waitUntil(ctx, g.lockIfSet)
-	}
+func (g *gate) waitAndLock(ctx context.Context) error {
 	select {
 	case <-g.set:
 		return nil
diff --git a/quic/gate_test.go b/quic/gate_test.go
index 54f7a8a..59c157d 100644
--- a/quic/gate_test.go
+++ b/quic/gate_test.go
@@ -47,7 +47,7 @@
 		time.Sleep(1 * time.Millisecond)
 		cancel()
 	}()
-	if err := g.waitAndLock(ctx, nil); err != context.Canceled {
+	if err := g.waitAndLock(ctx); err != context.Canceled {
 		t.Errorf("g.waitAndLock() = %v, want context.Canceled", err)
 	}
 	// waitAndLock succeeds
@@ -58,7 +58,7 @@
 		set = true
 		g.unlock(true)
 	}()
-	if err := g.waitAndLock(context.Background(), nil); err != nil {
+	if err := g.waitAndLock(context.Background()); err != nil {
 		t.Errorf("g.waitAndLock() = %v, want nil", err)
 	}
 	if !set {
@@ -66,7 +66,7 @@
 	}
 	g.unlock(true)
 	// waitAndLock succeeds when the gate is set and the context is canceled
-	if err := g.waitAndLock(ctx, nil); err != nil {
+	if err := g.waitAndLock(ctx); err != nil {
 		t.Errorf("g.waitAndLock() = %v, want nil", err)
 	}
 }
@@ -89,5 +89,5 @@
 		g.lock()
 		defer g.unlockFunc(func() bool { return true })
 	}()
-	g.waitAndLock(context.Background(), nil)
+	g.waitAndLock(context.Background())
 }
diff --git a/quic/idle_test.go b/quic/idle_test.go
index 29d3bd1..d9ae16a 100644
--- a/quic/idle_test.go
+++ b/quic/idle_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -9,10 +11,14 @@
 	"crypto/tls"
 	"fmt"
 	"testing"
+	"testing/synctest"
 	"time"
 )
 
 func TestHandshakeTimeoutExpiresServer(t *testing.T) {
+	synctest.Test(t, testHandshakeTimeoutExpiresServer)
+}
+func testHandshakeTimeoutExpiresServer(t *testing.T) {
 	const timeout = 5 * time.Second
 	tc := newTestConn(t, serverSide, func(c *Config) {
 		c.HandshakeTimeout = timeout
@@ -32,18 +38,18 @@
 		packetTypeHandshake, debugFrameCrypto{})
 	tc.writeAckForAll()
 
-	if got, want := tc.timerDelay(), timeout; got != want {
+	if got, want := tc.timeUntilEvent(), timeout; got != want {
 		t.Errorf("connection timer = %v, want %v (handshake timeout)", got, want)
 	}
 
 	// Client sends a packet, but this does not extend the handshake timer.
-	tc.advance(1 * time.Second)
+	time.Sleep(1 * time.Second)
 	tc.writeFrames(packetTypeHandshake, debugFrameCrypto{
 		data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][:1], // partial data
 	})
 	tc.wantIdle("handshake is not complete")
 
-	tc.advance(timeout - 1*time.Second)
+	time.Sleep(timeout - 1*time.Second)
 	tc.wantFrame("server closes connection after handshake timeout",
 		packetTypeHandshake, debugFrameConnectionCloseTransport{
 			code: errConnectionRefused,
@@ -51,6 +57,9 @@
 }
 
 func TestHandshakeTimeoutExpiresClient(t *testing.T) {
+	synctest.Test(t, testHandshakeTimeoutExpiresClient)
+}
+func testHandshakeTimeoutExpiresClient(t *testing.T) {
 	const timeout = 5 * time.Second
 	tc := newTestConn(t, clientSide, func(c *Config) {
 		c.HandshakeTimeout = timeout
@@ -77,10 +86,10 @@
 	tc.writeAckForAll()
 	tc.wantIdle("client is waiting for end of handshake")
 
-	if got, want := tc.timerDelay(), timeout; got != want {
+	if got, want := tc.timeUntilEvent(), timeout; got != want {
 		t.Errorf("connection timer = %v, want %v (handshake timeout)", got, want)
 	}
-	tc.advance(timeout)
+	time.Sleep(timeout)
 	tc.wantFrame("client closes connection after handshake timeout",
 		packetTypeHandshake, debugFrameConnectionCloseTransport{
 			code: errConnectionRefused,
@@ -110,7 +119,7 @@
 		wantTimeout:         10 * time.Second,
 	}} {
 		name := fmt.Sprintf("local=%v/peer=%v", test.localMaxIdleTimeout, test.peerMaxIdleTimeout)
-		t.Run(name, func(t *testing.T) {
+		synctestSubtest(t, name, func(t *testing.T) {
 			tc := newTestConn(t, serverSide, func(p *transportParameters) {
 				p.maxIdleTimeout = test.peerMaxIdleTimeout
 			}, func(c *Config) {
@@ -120,13 +129,13 @@
 			if got, want := tc.timeUntilEvent(), test.wantTimeout; got != want {
 				t.Errorf("new conn timeout=%v, want %v (idle timeout)", got, want)
 			}
-			tc.advance(test.wantTimeout - 1)
+			time.Sleep(test.wantTimeout - 1)
 			tc.wantIdle("connection is idle and alive prior to timeout")
 			ctx := canceledContext()
 			if err := tc.conn.Wait(ctx); err != context.Canceled {
 				t.Fatalf("conn.Wait() = %v, want Canceled", err)
 			}
-			tc.advance(1)
+			time.Sleep(1)
 			tc.wantIdle("connection exits after timeout")
 			if err := tc.conn.Wait(ctx); err != errIdleTimeout {
 				t.Fatalf("conn.Wait() = %v, want errIdleTimeout", err)
@@ -154,7 +163,7 @@
 		wantTimeout: 30 * time.Second,
 	}} {
 		name := fmt.Sprintf("idle_timeout=%v/keepalive=%v", test.idleTimeout, test.keepAlive)
-		t.Run(name, func(t *testing.T) {
+		synctestSubtest(t, name, func(t *testing.T) {
 			tc := newTestConn(t, serverSide, func(c *Config) {
 				c.MaxIdleTimeout = test.idleTimeout
 				c.KeepAlivePeriod = test.keepAlive
@@ -163,9 +172,9 @@
 			if got, want := tc.timeUntilEvent(), test.wantTimeout; got != want {
 				t.Errorf("new conn timeout=%v, want %v (keepalive timeout)", got, want)
 			}
-			tc.advance(test.wantTimeout - 1)
+			time.Sleep(test.wantTimeout - 1)
 			tc.wantIdle("connection is idle prior to timeout")
-			tc.advance(1)
+			time.Sleep(1)
 			tc.wantFrameType("keep-alive ping is sent", packetType1RTT,
 				debugFramePing{})
 		})
@@ -173,6 +182,9 @@
 }
 
 func TestIdleLongTermKeepAliveSent(t *testing.T) {
+	synctest.Test(t, testIdleLongTermKeepAliveSent)
+}
+func testIdleLongTermKeepAliveSent(t *testing.T) {
 	// This test examines a connection sitting idle and sending periodic keep-alive pings.
 	const keepAlivePeriod = 30 * time.Second
 	tc := newTestConn(t, clientSide, func(c *Config) {
@@ -191,7 +203,7 @@
 		if got, want := tc.timeUntilEvent(), keepAlivePeriod; got != want {
 			t.Errorf("i=%v conn timeout=%v, want %v (keepalive timeout)", i, got, want)
 		}
-		tc.advance(keepAlivePeriod)
+		time.Sleep(keepAlivePeriod)
 		tc.wantFrameType("keep-alive ping is sent", packetType1RTT,
 			debugFramePing{})
 		tc.writeAckForAll()
@@ -199,6 +211,9 @@
 }
 
 func TestIdleLongTermKeepAliveReceived(t *testing.T) {
+	synctest.Test(t, testIdleLongTermKeepAliveReceived)
+}
+func testIdleLongTermKeepAliveReceived(t *testing.T) {
 	// This test examines a connection sitting idle, but receiving periodic peer
 	// traffic to keep the connection alive.
 	const idleTimeout = 30 * time.Second
@@ -207,7 +222,7 @@
 	})
 	tc.handshake()
 	for i := 0; i < 10; i++ {
-		tc.advance(idleTimeout - 1*time.Second)
+		time.Sleep(idleTimeout - 1*time.Second)
 		tc.writeFrames(packetType1RTT, debugFramePing{})
 		if got, want := tc.timeUntilEvent(), maxAckDelay-timerGranularity; got != want {
 			t.Errorf("i=%v conn timeout=%v, want %v (max_ack_delay)", i, got, want)
diff --git a/quic/key_update_test.go b/quic/key_update_test.go
index 2daf7db..7a02e84 100644
--- a/quic/key_update_test.go
+++ b/quic/key_update_test.go
@@ -2,13 +2,19 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
 	"testing"
+	"testing/synctest"
 )
 
 func TestKeyUpdatePeerUpdates(t *testing.T) {
+	synctest.Test(t, testKeyUpdatePeerUpdates)
+}
+func testKeyUpdatePeerUpdates(t *testing.T) {
 	tc := newTestConn(t, serverSide)
 	tc.handshake()
 	tc.ignoreFrames = nil // ignore nothing
@@ -56,6 +62,9 @@
 }
 
 func TestKeyUpdateAcceptPreviousPhaseKeys(t *testing.T) {
+	synctest.Test(t, testKeyUpdateAcceptPreviousPhaseKeys)
+}
+func testKeyUpdateAcceptPreviousPhaseKeys(t *testing.T) {
 	// "An endpoint SHOULD retain old keys for some time after
 	// unprotecting a packet sent using the new keys."
 	// https://www.rfc-editor.org/rfc/rfc9001#section-6.1-8
@@ -112,6 +121,9 @@
 }
 
 func TestKeyUpdateRejectPacketFromPriorPhase(t *testing.T) {
+	synctest.Test(t, testKeyUpdateRejectPacketFromPriorPhase)
+}
+func testKeyUpdateRejectPacketFromPriorPhase(t *testing.T) {
 	// "Packets with higher packet numbers MUST be protected with either
 	// the same or newer packet protection keys than packets with lower packet numbers."
 	// https://www.rfc-editor.org/rfc/rfc9001#section-6.4-2
@@ -161,6 +173,9 @@
 }
 
 func TestKeyUpdateLocallyInitiated(t *testing.T) {
+	synctest.Test(t, testKeyUpdateLocallyInitiated)
+}
+func testKeyUpdateLocallyInitiated(t *testing.T) {
 	const updateAfter = 4 // initiate key update after 1-RTT packet 4
 	tc := newTestConn(t, serverSide)
 	tc.conn.keysAppData.updateAfter = updateAfter
diff --git a/quic/packet_codec_test.go b/quic/packet_codec_test.go
index 4ae22b3..d49f0ea 100644
--- a/quic/packet_codec_test.go
+++ b/quic/packet_codec_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
diff --git a/quic/path_test.go b/quic/path_test.go
index 60ff51e..16dd9fc 100644
--- a/quic/path_test.go
+++ b/quic/path_test.go
@@ -2,10 +2,13 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
 	"testing"
+	"testing/synctest"
 )
 
 func TestPathChallengeReceived(t *testing.T) {
@@ -22,30 +25,35 @@
 		padTo:       1200,
 		wantPadding: 1200,
 	}} {
-		// "The recipient of [a PATH_CHALLENGE] frame MUST generate
-		// a PATH_RESPONSE frame [...] containing the same Data value."
-		// https://www.rfc-editor.org/rfc/rfc9000.html#section-19.17-7
-		tc := newTestConn(t, clientSide)
-		tc.handshake()
-		tc.ignoreFrame(frameTypeAck)
-		data := pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}
-		tc.writeFrames(packetType1RTT, debugFramePathChallenge{
-			data: data,
-		}, debugFramePadding{
-			to: test.padTo,
-		})
-		tc.wantFrame("response to PATH_CHALLENGE",
-			packetType1RTT, debugFramePathResponse{
+		synctestSubtest(t, test.name, func(t *testing.T) {
+			// "The recipient of [a PATH_CHALLENGE] frame MUST generate
+			// a PATH_RESPONSE frame [...] containing the same Data value."
+			// https://www.rfc-editor.org/rfc/rfc9000.html#section-19.17-7
+			tc := newTestConn(t, clientSide)
+			tc.handshake()
+			tc.ignoreFrame(frameTypeAck)
+			data := pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}
+			tc.writeFrames(packetType1RTT, debugFramePathChallenge{
 				data: data,
+			}, debugFramePadding{
+				to: test.padTo,
 			})
-		if got, want := tc.lastDatagram.paddedSize, test.wantPadding; got != want {
-			t.Errorf("PATH_RESPONSE expanded to %v bytes, want %v", got, want)
-		}
-		tc.wantIdle("connection is idle")
+			tc.wantFrame("response to PATH_CHALLENGE",
+				packetType1RTT, debugFramePathResponse{
+					data: data,
+				})
+			if got, want := tc.lastDatagram.paddedSize, test.wantPadding; got != want {
+				t.Errorf("PATH_RESPONSE expanded to %v bytes, want %v", got, want)
+			}
+			tc.wantIdle("connection is idle")
+		})
 	}
 }
 
 func TestPathResponseMismatchReceived(t *testing.T) {
+	synctest.Test(t, testPathResponseMismatchReceived)
+}
+func testPathResponseMismatchReceived(t *testing.T) {
 	// "If the content of a PATH_RESPONSE frame does not match the content of
 	// a PATH_CHALLENGE frame previously sent by the endpoint,
 	// the endpoint MAY generate a connection error of type PROTOCOL_VIOLATION."
diff --git a/quic/ping_test.go b/quic/ping_test.go
index a8e6b61..4589a6c 100644
--- a/quic/ping_test.go
+++ b/quic/ping_test.go
@@ -2,11 +2,19 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
-import "testing"
+import (
+	"testing"
+	"testing/synctest"
+)
 
 func TestPing(t *testing.T) {
+	synctest.Test(t, testPing)
+}
+func testPing(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	tc.handshake()
 
@@ -22,6 +30,9 @@
 }
 
 func TestAck(t *testing.T) {
+	synctest.Test(t, testAck)
+}
+func testAck(t *testing.T) {
 	tc := newTestConn(t, serverSide)
 	tc.handshake()
 
diff --git a/quic/qlog_test.go b/quic/qlog_test.go
index 08c2a77..47e4671 100644
--- a/quic/qlog_test.go
+++ b/quic/qlog_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -12,14 +14,16 @@
 	"io"
 	"log/slog"
 	"reflect"
+	"sync"
 	"testing"
+	"testing/synctest"
 	"time"
 
 	"golang.org/x/net/quic/qlog"
 )
 
 func TestQLogHandshake(t *testing.T) {
-	testSides(t, "", func(t *testing.T, side connSide) {
+	testSidesSynctest(t, "", func(t *testing.T, side connSide) {
 		qr := &qlogRecord{}
 		tc := newTestConn(t, side, qr.config)
 		tc.handshake()
@@ -55,6 +59,9 @@
 }
 
 func TestQLogPacketFrames(t *testing.T) {
+	synctest.Test(t, testQLogPacketFrames)
+}
+func testQLogPacketFrames(t *testing.T) {
 	qr := &qlogRecord{}
 	tc := newTestConn(t, clientSide, qr.config)
 	tc.handshake()
@@ -111,7 +118,7 @@
 			tc.ignoreFrame(frameTypeCrypto)
 			tc.ignoreFrame(frameTypeAck)
 			tc.ignoreFrame(frameTypePing)
-			tc.advance(5 * time.Second)
+			time.Sleep(5 * time.Second)
 		},
 	}, {
 		trigger: "idle_timeout",
@@ -122,7 +129,7 @@
 		},
 		f: func(tc *testConn) {
 			tc.handshake()
-			tc.advance(5 * time.Second)
+			time.Sleep(5 * time.Second)
 		},
 	}, {
 		trigger: "error",
@@ -134,7 +141,7 @@
 			tc.conn.Abort(nil)
 		},
 	}} {
-		t.Run(test.trigger, func(t *testing.T) {
+		synctestSubtest(t, test.trigger, func(t *testing.T) {
 			qr := &qlogRecord{}
 			tc := newTestConn(t, clientSide, append(test.connOpts, qr.config)...)
 			test.f(tc)
@@ -147,7 +154,7 @@
 				t.Fatalf("unexpected frame: %v", fr)
 			}
 			tc.wantIdle("connection should be idle while closing")
-			tc.advance(5 * time.Second) // long enough for the drain timer to expire
+			time.Sleep(5 * time.Second) // long enough for the drain timer to expire
 			qr.wantEvents(t, jsonEvent{
 				"name": "connectivity:connection_closed",
 				"data": map[string]any{
@@ -159,6 +166,9 @@
 }
 
 func TestQLogRecovery(t *testing.T) {
+	synctest.Test(t, testQLogRecovery)
+}
+func testQLogRecovery(t *testing.T) {
 	qr := &qlogRecord{}
 	tc, s := newTestConnAndLocalStream(t, clientSide, uniStream,
 		permissiveTransportParameters, qr.config)
@@ -198,6 +208,9 @@
 }
 
 func TestQLogLoss(t *testing.T) {
+	synctest.Test(t, testQLogLoss)
+}
+func testQLogLoss(t *testing.T) {
 	qr := &qlogRecord{}
 	tc, s := newTestConnAndLocalStream(t, clientSide, uniStream,
 		permissiveTransportParameters, qr.config)
@@ -230,6 +243,9 @@
 }
 
 func TestQLogPacketDropped(t *testing.T) {
+	synctest.Test(t, testQLogPacketDropped)
+}
+func testQLogPacketDropped(t *testing.T) {
 	qr := &qlogRecord{}
 	tc := newTestConn(t, clientSide, permissiveTransportParameters, qr.config)
 	tc.handshake()
@@ -324,10 +340,13 @@
 
 // A qlogRecord records events.
 type qlogRecord struct {
+	mu sync.Mutex
 	ev []jsonEvent
 }
 
 func (q *qlogRecord) Write(b []byte) (int, error) {
+	q.mu.Lock()
+	defer q.mu.Unlock()
 	// This relies on the property that the Handler always makes one Write call per event.
 	if len(b) < 1 || b[0] != 0x1e {
 		panic(fmt.Errorf("trace Write should start with record separator, got %q", string(b)))
@@ -355,6 +374,8 @@
 // wantEvents checks that every event in want occurs in the order specified.
 func (q *qlogRecord) wantEvents(t *testing.T, want ...jsonEvent) {
 	t.Helper()
+	q.mu.Lock()
+	defer q.mu.Unlock()
 	got := q.ev
 	if !jsonPartialEqual(got, want) {
 		t.Fatalf("got events:\n%v\n\nwant events:\n%v", got, want)
diff --git a/quic/queue.go b/quic/queue.go
index 8b90ae7..f2712f4 100644
--- a/quic/queue.go
+++ b/quic/queue.go
@@ -42,9 +42,9 @@
 
 // get removes the first item from the queue, blocking until ctx is done, an item is available,
 // or the queue is closed.
-func (q *queue[T]) get(ctx context.Context, testHooks connTestHooks) (T, error) {
+func (q *queue[T]) get(ctx context.Context) (T, error) {
 	var zero T
-	if err := q.gate.waitAndLock(ctx, testHooks); err != nil {
+	if err := q.gate.waitAndLock(ctx); err != nil {
 		return zero, err
 	}
 	defer q.unlock()
diff --git a/quic/queue_test.go b/quic/queue_test.go
index b583521..a3907f3 100644
--- a/quic/queue_test.go
+++ b/quic/queue_test.go
@@ -16,7 +16,7 @@
 	cancel()
 
 	q := newQueue[int]()
-	if got, err := q.get(nonblocking, nil); err != context.Canceled {
+	if got, err := q.get(nonblocking); err != context.Canceled {
 		t.Fatalf("q.get() = %v, %v, want nil, context.Canceled", got, err)
 	}
 
@@ -26,13 +26,13 @@
 	if !q.put(2) {
 		t.Fatalf("q.put(2) = false, want true")
 	}
-	if got, err := q.get(nonblocking, nil); got != 1 || err != nil {
+	if got, err := q.get(nonblocking); got != 1 || err != nil {
 		t.Fatalf("q.get() = %v, %v, want 1, nil", got, err)
 	}
-	if got, err := q.get(nonblocking, nil); got != 2 || err != nil {
+	if got, err := q.get(nonblocking); got != 2 || err != nil {
 		t.Fatalf("q.get() = %v, %v, want 2, nil", got, err)
 	}
-	if got, err := q.get(nonblocking, nil); err != context.Canceled {
+	if got, err := q.get(nonblocking); err != context.Canceled {
 		t.Fatalf("q.get() = %v, %v, want nil, context.Canceled", got, err)
 	}
 
@@ -40,7 +40,7 @@
 		time.Sleep(1 * time.Millisecond)
 		q.put(3)
 	}()
-	if got, err := q.get(context.Background(), nil); got != 3 || err != nil {
+	if got, err := q.get(context.Background()); got != 3 || err != nil {
 		t.Fatalf("q.get() = %v, %v, want 3, nil", got, err)
 	}
 
@@ -48,7 +48,7 @@
 		t.Fatalf("q.put(2) = false, want true")
 	}
 	q.close(io.EOF)
-	if got, err := q.get(context.Background(), nil); got != 0 || err != io.EOF {
+	if got, err := q.get(context.Background()); got != 0 || err != io.EOF {
 		t.Fatalf("q.get() = %v, %v, want 0, io.EOF", got, err)
 	}
 	if q.put(5) {
diff --git a/quic/quic_test.go b/quic/quic_test.go
index 071003e..cdcc0d7 100644
--- a/quic/quic_test.go
+++ b/quic/quic_test.go
@@ -2,10 +2,13 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
 	"testing"
+	"testing/synctest"
 )
 
 func testSides(t *testing.T, name string, f func(*testing.T, connSide)) {
@@ -16,6 +19,16 @@
 	t.Run(name+"client", func(t *testing.T) { f(t, clientSide) })
 }
 
+func testSidesSynctest(t *testing.T, name string, f func(*testing.T, connSide)) {
+	t.Helper()
+	testSides(t, name, func(t *testing.T, side connSide) {
+		t.Helper()
+		synctest.Test(t, func(t *testing.T) {
+			f(t, side)
+		})
+	})
+}
+
 func testStreamTypes(t *testing.T, name string, f func(*testing.T, streamType)) {
 	if name != "" {
 		name += "/"
@@ -24,6 +37,16 @@
 	t.Run(name+"uni", func(t *testing.T) { f(t, uniStream) })
 }
 
+func testStreamTypesSynctest(t *testing.T, name string, f func(*testing.T, streamType)) {
+	t.Helper()
+	testStreamTypes(t, name, func(t *testing.T, stype streamType) {
+		t.Helper()
+		synctest.Test(t, func(t *testing.T) {
+			f(t, stype)
+		})
+	})
+}
+
 func testSidesAndStreamTypes(t *testing.T, name string, f func(*testing.T, connSide, streamType)) {
 	if name != "" {
 		name += "/"
@@ -33,3 +56,20 @@
 	t.Run(name+"server/uni", func(t *testing.T) { f(t, serverSide, uniStream) })
 	t.Run(name+"client/uni", func(t *testing.T) { f(t, clientSide, uniStream) })
 }
+
+func testSidesAndStreamTypesSynctest(t *testing.T, name string, f func(*testing.T, connSide, streamType)) {
+	t.Helper()
+	testSidesAndStreamTypes(t, name, func(t *testing.T, side connSide, stype streamType) {
+		t.Helper()
+		synctest.Test(t, func(t *testing.T) {
+			f(t, side, stype)
+		})
+	})
+}
+
+func synctestSubtest(t *testing.T, name string, f func(t *testing.T)) {
+	t.Run(name, func(t *testing.T) {
+		t.Helper()
+		synctest.Test(t, f)
+	})
+}
diff --git a/quic/retry_test.go b/quic/retry_test.go
index d6f0254..7a4481c 100644
--- a/quic/retry_test.go
+++ b/quic/retry_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -10,6 +12,7 @@
 	"crypto/tls"
 	"net/netip"
 	"testing"
+	"testing/synctest"
 	"time"
 )
 
@@ -77,9 +80,12 @@
 }
 
 func TestRetryServerSucceeds(t *testing.T) {
+	synctest.Test(t, testRetryServerSucceeds)
+}
+func testRetryServerSucceeds(t *testing.T) {
 	rt := newRetryServerTest(t)
 	te := rt.te
-	te.advance(retryTokenValidityPeriod)
+	time.Sleep(retryTokenValidityPeriod)
 	te.writeDatagram(&testDatagram{
 		packets: []*testPacket{{
 			ptype:     packetTypeInitial,
@@ -117,6 +123,9 @@
 }
 
 func TestRetryServerTokenInvalid(t *testing.T) {
+	synctest.Test(t, testRetryServerTokenInvalid)
+}
+func testRetryServerTokenInvalid(t *testing.T) {
 	// "If a server receives a client Initial that contains an invalid Retry token [...]
 	// the server SHOULD immediately close [...] the connection with an
 	// INVALID_TOKEN error."
@@ -147,11 +156,14 @@
 }
 
 func TestRetryServerTokenTooOld(t *testing.T) {
+	synctest.Test(t, testRetryServerTokenTooOld)
+}
+func testRetryServerTokenTooOld(t *testing.T) {
 	// "[...] a token SHOULD have an expiration time [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-8.1.3-3
 	rt := newRetryServerTest(t)
 	te := rt.te
-	te.advance(retryTokenValidityPeriod + time.Second)
+	time.Sleep(retryTokenValidityPeriod + time.Second)
 	te.writeDatagram(&testDatagram{
 		packets: []*testPacket{{
 			ptype:     packetTypeInitial,
@@ -176,6 +188,9 @@
 }
 
 func TestRetryServerTokenWrongIP(t *testing.T) {
+	synctest.Test(t, testRetryServerTokenWrongIP)
+}
+func testRetryServerTokenWrongIP(t *testing.T) {
 	// "Tokens sent in Retry packets SHOULD include information that allows the server
 	// to verify that the source IP address and port in client packets remain constant."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-8.1.4-3
@@ -206,6 +221,9 @@
 }
 
 func TestRetryServerIgnoresRetry(t *testing.T) {
+	synctest.Test(t, testRetryServerIgnoresRetry)
+}
+func testRetryServerIgnoresRetry(t *testing.T) {
 	tc := newTestConn(t, serverSide)
 	tc.handshake()
 	tc.write(&testDatagram{
@@ -225,6 +243,9 @@
 }
 
 func TestRetryClientSuccess(t *testing.T) {
+	synctest.Test(t, testRetryClientSuccess)
+}
+func testRetryClientSuccess(t *testing.T) {
 	// "This token MUST be repeated by the client in all Initial packets it sends
 	// for that connection after it receives the Retry packet."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-1
@@ -323,7 +344,7 @@
 			p.retrySrcConnID = []byte("invalid")
 		},
 	}} {
-		t.Run(test.name, func(t *testing.T) {
+		synctestSubtest(t, test.name, func(t *testing.T) {
 			tc := newTestConn(t, clientSide,
 				func(p *transportParameters) {
 					p.initialSrcConnID = initialSrcConnID
@@ -367,6 +388,9 @@
 }
 
 func TestRetryClientIgnoresRetryAfterReceivingPacket(t *testing.T) {
+	synctest.Test(t, testRetryClientIgnoresRetryAfterReceivingPacket)
+}
+func testRetryClientIgnoresRetryAfterReceivingPacket(t *testing.T) {
 	// "After the client has received and processed an Initial or Retry packet
 	// from the server, it MUST discard any subsequent Retry packets that it receives."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1
@@ -401,6 +425,9 @@
 }
 
 func TestRetryClientIgnoresRetryAfterReceivingRetry(t *testing.T) {
+	synctest.Test(t, testRetryClientIgnoresRetryAfterReceivingRetry)
+}
+func testRetryClientIgnoresRetryAfterReceivingRetry(t *testing.T) {
 	// "After the client has received and processed an Initial or Retry packet
 	// from the server, it MUST discard any subsequent Retry packets that it receives."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1
@@ -424,6 +451,9 @@
 }
 
 func TestRetryClientIgnoresRetryWithInvalidIntegrityTag(t *testing.T) {
+	synctest.Test(t, testRetryClientIgnoresRetryWithInvalidIntegrityTag)
+}
+func testRetryClientIgnoresRetryWithInvalidIntegrityTag(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	tc.wantFrameType("client Initial CRYPTO data",
 		packetTypeInitial, debugFrameCrypto{})
@@ -441,6 +471,9 @@
 }
 
 func TestRetryClientIgnoresRetryWithZeroLengthToken(t *testing.T) {
+	synctest.Test(t, testRetryClientIgnoresRetryWithZeroLengthToken)
+}
+func testRetryClientIgnoresRetryWithZeroLengthToken(t *testing.T) {
 	// "A client MUST discard a Retry packet with a zero-length Retry Token field."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-2
 	tc := newTestConn(t, clientSide)
diff --git a/quic/skip_test.go b/quic/skip_test.go
index 1fcb735..2c33378 100644
--- a/quic/skip_test.go
+++ b/quic/skip_test.go
@@ -2,11 +2,19 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
-import "testing"
+import (
+	"testing"
+	"testing/synctest"
+)
 
 func TestSkipPackets(t *testing.T) {
+	synctest.Test(t, testSkipPackets)
+}
+func testSkipPackets(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters)
 	connWritesPacket := func() {
 		s.WriteByte(0)
@@ -39,6 +47,9 @@
 }
 
 func TestSkipAckForSkippedPacket(t *testing.T) {
+	synctest.Test(t, testSkipAckForSkippedPacket)
+}
+func testSkipAckForSkippedPacket(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters)
 
 	// Cause the connection to send packets until it skips a packet number.
diff --git a/quic/stateless_reset_test.go b/quic/stateless_reset_test.go
index 33d467a..9473750 100644
--- a/quic/stateless_reset_test.go
+++ b/quic/stateless_reset_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -12,10 +14,14 @@
 	"errors"
 	"net/netip"
 	"testing"
+	"testing/synctest"
 	"time"
 )
 
 func TestStatelessResetClientSendsStatelessResetTokenTransportParameter(t *testing.T) {
+	synctest.Test(t, testStatelessResetClientSendsStatelessResetTokenTransportParameter)
+}
+func testStatelessResetClientSendsStatelessResetTokenTransportParameter(t *testing.T) {
 	// "[The stateless_reset_token] transport parameter MUST NOT be sent by a client [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-18.2-4.6.1
 	resetToken := testPeerStatelessResetToken(0)
@@ -61,6 +67,9 @@
 }
 
 func TestStatelessResetSentSizes(t *testing.T) {
+	synctest.Test(t, testStatelessResetSentSizes)
+}
+func testStatelessResetSentSizes(t *testing.T) {
 	config := &Config{
 		TLSConfig:         newTestTLSConfig(serverSide),
 		StatelessResetKey: testStatelessResetKey,
@@ -126,6 +135,9 @@
 }
 
 func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) {
+	synctest.Test(t, testStatelessResetSuccessfulNewConnectionID)
+}
+func testStatelessResetSuccessfulNewConnectionID(t *testing.T) {
 	// "[...] Stateless Reset Token field values from [...] NEW_CONNECTION_ID frames [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-1
 	qr := &qlogRecord{}
@@ -155,7 +167,7 @@
 		t.Errorf("conn.Wait() = %v, want errStatelessReset", err)
 	}
 	tc.wantIdle("closed connection is idle in draining")
-	tc.advance(1 * time.Second) // long enough to exit the draining state
+	time.Sleep(1 * time.Second) // long enough to exit the draining state
 	tc.wantIdle("closed connection is idle after draining")
 
 	qr.wantEvents(t, jsonEvent{
@@ -167,6 +179,9 @@
 }
 
 func TestStatelessResetSuccessfulTransportParameter(t *testing.T) {
+	synctest.Test(t, testStatelessResetSuccessfulTransportParameter)
+}
+func testStatelessResetSuccessfulTransportParameter(t *testing.T) {
 	// "[...] Stateless Reset Token field values from [...]
 	// the server's transport parameters [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-1
@@ -229,7 +244,7 @@
 		}, testLocalConnID(0)...),
 		size: 100,
 	}} {
-		t.Run(test.name, func(t *testing.T) {
+		synctestSubtest(t, test.name, func(t *testing.T) {
 			resetToken := testPeerStatelessResetToken(0)
 			tc := newTestConn(t, clientSide, func(p *transportParameters) {
 				p.statelessResetToken = resetToken[:]
@@ -252,6 +267,9 @@
 }
 
 func TestStatelessResetRetiredConnID(t *testing.T) {
+	synctest.Test(t, testStatelessResetRetiredConnID)
+}
+func testStatelessResetRetiredConnID(t *testing.T) {
 	// "An endpoint MUST NOT check for any stateless reset tokens [...]
 	// for connection IDs that have been retired."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-3
diff --git a/quic/stream.go b/quic/stream.go
index b20cfe7..4c63207 100644
--- a/quic/stream.go
+++ b/quic/stream.go
@@ -236,7 +236,7 @@
 		s.inbufoff += n
 		return n, nil
 	}
-	if err := s.ingate.waitAndLock(s.inctx, s.conn.testHooks); err != nil {
+	if err := s.ingate.waitAndLock(s.inctx); err != nil {
 		return 0, err
 	}
 	if s.inbufoff > 0 {
@@ -350,7 +350,7 @@
 		if len(b) > 0 && !canWrite {
 			// Our send buffer is full. Wait for the peer to ack some data.
 			s.outUnlock()
-			if err := s.outgate.waitAndLock(s.outctx, s.conn.testHooks); err != nil {
+			if err := s.outgate.waitAndLock(s.outctx); err != nil {
 				return n, err
 			}
 			// Successfully returning from waitAndLockGate means we are no longer
diff --git a/quic/stream_limits.go b/quic/stream_limits.go
index ed31c36..f1abcae 100644
--- a/quic/stream_limits.go
+++ b/quic/stream_limits.go
@@ -29,7 +29,7 @@
 // open creates a new local stream, blocking until MAX_STREAMS quota is available.
 func (lim *localStreamLimits) open(ctx context.Context, c *Conn) (num int64, err error) {
 	// TODO: Send a STREAMS_BLOCKED when blocked.
-	if err := lim.gate.waitAndLock(ctx, c.testHooks); err != nil {
+	if err := lim.gate.waitAndLock(ctx); err != nil {
 		return 0, err
 	}
 	if lim.opened < 0 {
diff --git a/quic/stream_limits_test.go b/quic/stream_limits_test.go
index ad63411..d62b29b 100644
--- a/quic/stream_limits_test.go
+++ b/quic/stream_limits_test.go
@@ -2,19 +2,22 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
 	"context"
 	"crypto/tls"
 	"testing"
+	"testing/synctest"
 )
 
 func TestStreamLimitNewStreamBlocked(t *testing.T) {
 	// "An endpoint that receives a frame with a stream ID exceeding the limit
 	// it has sent MUST treat this as a connection error of type STREAM_LIMIT_ERROR [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-4.6-3
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		ctx := canceledContext()
 		tc := newTestConn(t, clientSide,
 			permissiveTransportParameters,
@@ -46,7 +49,7 @@
 func TestStreamLimitMaxStreamsDecreases(t *testing.T) {
 	// "MAX_STREAMS frames that do not increase the stream limit MUST be ignored."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-4.6-4
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		ctx := canceledContext()
 		tc := newTestConn(t, clientSide,
 			permissiveTransportParameters,
@@ -77,7 +80,7 @@
 }
 
 func TestStreamLimitViolated(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc := newTestConn(t, serverSide,
 			func(c *Config) {
 				if styp == bidiStream {
@@ -104,7 +107,7 @@
 }
 
 func TestStreamLimitImplicitStreams(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc := newTestConn(t, serverSide,
 			func(c *Config) {
 				c.MaxBidiRemoteStreams = 1 << 60
@@ -152,7 +155,7 @@
 	// a value greater than 2^60 [...] the connection MUST be closed
 	// immediately with a connection error of type TRANSPORT_PARAMETER_ERROR [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-4.6-2
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc := newTestConn(t, serverSide,
 			func(p *transportParameters) {
 				if styp == bidiStream {
@@ -177,7 +180,7 @@
 	// greater than 2^60 [...] the connection MUST be closed immediately
 	// with a connection error [...] of type FRAME_ENCODING_ERROR [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-4.6-2
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc := newTestConn(t, serverSide)
 		tc.handshake()
 		tc.writeFrames(packetTypeInitial,
@@ -197,7 +200,7 @@
 }
 
 func TestStreamLimitSendUpdatesMaxStreams(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc := newTestConn(t, serverSide, func(c *Config) {
 			if styp == uniStream {
 				c.MaxUniRemoteStreams = 4
@@ -236,6 +239,9 @@
 }
 
 func TestStreamLimitStopSendingDoesNotUpdateMaxStreams(t *testing.T) {
+	synctest.Test(t, testStreamLimitStopSendingDoesNotUpdateMaxStreams)
+}
+func testStreamLimitStopSendingDoesNotUpdateMaxStreams(t *testing.T) {
 	tc, s := newTestConnAndRemoteStream(t, serverSide, bidiStream, func(c *Config) {
 		c.MaxBidiRemoteStreams = 1
 	})
diff --git a/quic/stream_test.go b/quic/stream_test.go
index 4119cc1..67d17f6 100644
--- a/quic/stream_test.go
+++ b/quic/stream_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -13,12 +15,13 @@
 	"io"
 	"strings"
 	"testing"
+	"testing/synctest"
 
 	"golang.org/x/net/internal/quic/quicwire"
 )
 
 func TestStreamWriteBlockedByOutputBuffer(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
 		const writeBufferSize = 4
 		tc := newTestConn(t, clientSide, permissiveTransportParameters, func(c *Config) {
@@ -79,7 +82,7 @@
 }
 
 func TestStreamWriteBlockedByStreamFlowControl(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		ctx := canceledContext()
 		want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
 		tc := newTestConn(t, clientSide, func(p *transportParameters) {
@@ -149,7 +152,7 @@
 	// "A sender MUST ignore any MAX_STREAM_DATA [...] frames that
 	// do not increase flow control limits."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-4.1-9
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		ctx := canceledContext()
 		want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
 		tc := newTestConn(t, clientSide, func(p *transportParameters) {
@@ -218,7 +221,7 @@
 }
 
 func TestStreamWriteBlockedByWriteBufferLimit(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
 		const maxWriteBuffer = 4
 		tc := newTestConn(t, clientSide, func(p *transportParameters) {
@@ -392,7 +395,7 @@
 			wantEOF: true,
 		}},
 	}} {
-		testStreamTypes(t, test.name, func(t *testing.T, styp streamType) {
+		testStreamTypesSynctest(t, test.name, func(t *testing.T, styp streamType) {
 			tc := newTestConn(t, serverSide)
 			tc.handshake()
 			sid := newStreamID(clientSide, styp, 0)
@@ -439,7 +442,7 @@
 }
 
 func TestStreamReceiveExtendsStreamWindow(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		const maxWindowSize = 20
 		ctx := canceledContext()
 		tc := newTestConn(t, serverSide, func(c *Config) {
@@ -484,7 +487,7 @@
 	// "A receiver MUST close the connection with an error of type FLOW_CONTROL_ERROR if
 	// the sender violates the advertised [...] stream data limits [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-4.1-8
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		const maxStreamData = 10
 		for _, test := range []struct {
 			off  int64
@@ -521,7 +524,7 @@
 }
 
 func TestStreamReceiveDuplicateDataDoesNotViolateLimits(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		const maxData = 10
 		tc := newTestConn(t, serverSide, func(c *Config) {
 			// TODO: Add connection-level maximum data here as well.
@@ -544,7 +547,7 @@
 	// A stream receives some data, we read a byte of that data
 	// (causing the rest to be pulled into the s.inbuf buffer),
 	// and then we receive a FIN with no additional data.
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc, s := newTestConnAndRemoteStream(t, serverSide, styp, permissiveTransportParameters)
 		want := []byte{1, 2, 3}
 		tc.writeFrames(packetType1RTT, debugFrameStream{
@@ -568,7 +571,7 @@
 
 func TestStreamReadByteFromOneByteStream(t *testing.T) {
 	// ReadByte on the only byte of a stream should not return an error.
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc, s := newTestConnAndRemoteStream(t, serverSide, styp, permissiveTransportParameters)
 		want := byte(1)
 		tc.writeFrames(packetType1RTT, debugFrameStream{
@@ -608,7 +611,7 @@
 				})
 			},
 		}} {
-			t.Run(test.name, func(t *testing.T) {
+			synctestSubtest(t, test.name, func(t *testing.T) {
 				tc := newTestConn(t, serverSide, opts...)
 				tc.handshake()
 				sid := newStreamID(clientSide, styp, 0)
@@ -662,7 +665,7 @@
 	// "A receiver SHOULD treat receipt of data at or beyond
 	// the final size as an error of type FINAL_SIZE_ERROR [...]"
 	// https://www.rfc-editor.org/rfc/rfc9000#section-4.5-5
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc := newTestConn(t, serverSide)
 		tc.handshake()
 		sid := newStreamID(clientSide, styp, 0)
@@ -688,7 +691,7 @@
 }
 
 func TestStreamReceiveUnblocksReader(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc := newTestConn(t, serverSide)
 		tc.handshake()
 		want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
@@ -746,7 +749,7 @@
 // It then sends the returned frame (STREAM, STREAM_DATA_BLOCKED, etc.)
 // to the conn and expects a STREAM_STATE_ERROR.
 func testStreamSendFrameInvalidState(t *testing.T, f func(sid streamID) debugFrame) {
-	testSides(t, "stream_not_created", func(t *testing.T, side connSide) {
+	testSidesSynctest(t, "stream_not_created", func(t *testing.T, side connSide) {
 		tc := newTestConn(t, side, permissiveTransportParameters)
 		tc.handshake()
 		tc.writeFrames(packetType1RTT, f(newStreamID(side, bidiStream, 0)))
@@ -755,7 +758,7 @@
 				code: errStreamState,
 			})
 	})
-	testSides(t, "uni_stream", func(t *testing.T, side connSide) {
+	testSidesSynctest(t, "uni_stream", func(t *testing.T, side connSide) {
 		ctx := canceledContext()
 		tc := newTestConn(t, side, permissiveTransportParameters)
 		tc.handshake()
@@ -823,7 +826,7 @@
 // It then sends the returned frame (MAX_STREAM_DATA, STOP_SENDING, etc.)
 // to the conn and expects a STREAM_STATE_ERROR.
 func testStreamReceiveFrameInvalidState(t *testing.T, f func(sid streamID) debugFrame) {
-	testSides(t, "stream_not_created", func(t *testing.T, side connSide) {
+	testSidesSynctest(t, "stream_not_created", func(t *testing.T, side connSide) {
 		tc := newTestConn(t, side)
 		tc.handshake()
 		tc.writeFrames(packetType1RTT, f(newStreamID(side, bidiStream, 0)))
@@ -832,7 +835,7 @@
 				code: errStreamState,
 			})
 	})
-	testSides(t, "uni_stream", func(t *testing.T, side connSide) {
+	testSidesSynctest(t, "uni_stream", func(t *testing.T, side connSide) {
 		tc := newTestConn(t, side)
 		tc.handshake()
 		tc.writeFrames(packetType1RTT, f(newStreamID(side.peer(), uniStream, 0)))
@@ -873,6 +876,9 @@
 }
 
 func TestStreamOffsetTooLarge(t *testing.T) {
+	synctest.Test(t, testStreamOffsetTooLarge)
+}
+func testStreamOffsetTooLarge(t *testing.T) {
 	// "Receipt of a frame that exceeds [2^62-1] MUST be treated as a
 	// connection error of type FRAME_ENCODING_ERROR or FLOW_CONTROL_ERROR."
 	// https://www.rfc-editor.org/rfc/rfc9000.html#section-19.8-9
@@ -894,6 +900,9 @@
 }
 
 func TestStreamReadFromWriteOnlyStream(t *testing.T) {
+	synctest.Test(t, testStreamReadFromWriteOnlyStream)
+}
+func testStreamReadFromWriteOnlyStream(t *testing.T) {
 	_, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters)
 	buf := make([]byte, 10)
 	wantErr := "read from write-only stream"
@@ -903,6 +912,9 @@
 }
 
 func TestStreamWriteToReadOnlyStream(t *testing.T) {
+	synctest.Test(t, testStreamWriteToReadOnlyStream)
+}
+func testStreamWriteToReadOnlyStream(t *testing.T) {
 	_, s := newTestConnAndRemoteStream(t, serverSide, uniStream)
 	buf := make([]byte, 10)
 	wantErr := "write to read-only stream"
@@ -912,6 +924,9 @@
 }
 
 func TestStreamReadFromClosedStream(t *testing.T) {
+	synctest.Test(t, testStreamReadFromClosedStream)
+}
+func testStreamReadFromClosedStream(t *testing.T) {
 	tc, s := newTestConnAndRemoteStream(t, serverSide, bidiStream, permissiveTransportParameters)
 	s.CloseRead()
 	tc.wantFrame("CloseRead sends a STOP_SENDING frame",
@@ -934,6 +949,9 @@
 }
 
 func TestStreamCloseReadWithAllDataReceived(t *testing.T) {
+	synctest.Test(t, testStreamCloseReadWithAllDataReceived)
+}
+func testStreamCloseReadWithAllDataReceived(t *testing.T) {
 	tc, s := newTestConnAndRemoteStream(t, serverSide, bidiStream, permissiveTransportParameters)
 	tc.writeFrames(packetType1RTT, debugFrameStream{
 		id:   s.id,
@@ -950,6 +968,9 @@
 }
 
 func TestStreamWriteToClosedStream(t *testing.T) {
+	synctest.Test(t, testStreamWriteToClosedStream)
+}
+func testStreamWriteToClosedStream(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, serverSide, bidiStream, permissiveTransportParameters)
 	s.CloseWrite()
 	tc.wantFrame("stream is opened after being closed",
@@ -966,6 +987,9 @@
 }
 
 func TestStreamResetBlockedStream(t *testing.T) {
+	synctest.Test(t, testStreamResetBlockedStream)
+}
+func testStreamResetBlockedStream(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, serverSide, bidiStream, permissiveTransportParameters,
 		func(c *Config) {
 			c.MaxStreamWriteBufferSize = 4
@@ -1002,6 +1026,9 @@
 }
 
 func TestStreamWriteMoreThanOnePacketOfData(t *testing.T) {
+	synctest.Test(t, testStreamWriteMoreThanOnePacketOfData)
+}
+func testStreamWriteMoreThanOnePacketOfData(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) {
 		p.initialMaxStreamsUni = 1
 		p.initialMaxData = 1 << 20
@@ -1038,6 +1065,9 @@
 }
 
 func TestStreamCloseWaitsForAcks(t *testing.T) {
+	synctest.Test(t, testStreamCloseWaitsForAcks)
+}
+func testStreamCloseWaitsForAcks(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters)
 	data := make([]byte, 100)
 	s.Write(data)
@@ -1071,6 +1101,9 @@
 }
 
 func TestStreamCloseReadOnly(t *testing.T) {
+	synctest.Test(t, testStreamCloseReadOnly)
+}
+func testStreamCloseReadOnly(t *testing.T) {
 	tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, permissiveTransportParameters)
 	if err := s.Close(); err != nil {
 		t.Errorf("s.Close() = %v, want nil", err)
@@ -1103,10 +1136,10 @@
 		name: "stream reset",
 		unblock: func(tc *testConn, s *Stream) {
 			s.Reset(0)
-			tc.wait() // wait for test conn to process the Reset
+			synctest.Wait() // wait for test conn to process the Reset
 		},
 	}} {
-		t.Run(test.name, func(t *testing.T) {
+		synctestSubtest(t, test.name, func(t *testing.T) {
 			tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters)
 			data := make([]byte, 100)
 			s.Write(data)
@@ -1148,6 +1181,9 @@
 }
 
 func TestStreamCloseWriteWhenBlockedByStreamFlowControl(t *testing.T) {
+	synctest.Test(t, testStreamCloseWriteWhenBlockedByStreamFlowControl)
+}
+func testStreamCloseWriteWhenBlockedByStreamFlowControl(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters,
 		func(p *transportParameters) {
 			//p.initialMaxData = 0
@@ -1185,7 +1221,7 @@
 }
 
 func TestStreamPeerResetsWithUnreadAndUnsentData(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc, s := newTestConnAndRemoteStream(t, serverSide, styp)
 		data := []byte{0, 1, 2, 3, 4, 5, 6, 7}
 		tc.writeFrames(packetType1RTT, debugFrameStream{
@@ -1210,7 +1246,7 @@
 }
 
 func TestStreamPeerResetWakesBlockedRead(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc, s := newTestConnAndRemoteStream(t, serverSide, styp)
 		reader := runAsync(tc, func(ctx context.Context) (int, error) {
 			s.SetReadContext(ctx)
@@ -1231,7 +1267,7 @@
 }
 
 func TestStreamPeerResetFollowedByData(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc, s := newTestConnAndRemoteStream(t, serverSide, styp)
 		tc.writeFrames(packetType1RTT, debugFrameResetStream{
 			id:        s.id,
@@ -1256,6 +1292,9 @@
 }
 
 func TestStreamResetInvalidCode(t *testing.T) {
+	synctest.Test(t, testStreamResetInvalidCode)
+}
+func testStreamResetInvalidCode(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters)
 	s.Reset(1 << 62)
 	tc.wantFrame("reset with invalid code sends a RESET_STREAM anyway",
@@ -1268,6 +1307,9 @@
 }
 
 func TestStreamResetReceiveOnly(t *testing.T) {
+	synctest.Test(t, testStreamResetReceiveOnly)
+}
+func testStreamResetReceiveOnly(t *testing.T) {
 	tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream)
 	s.Reset(0)
 	tc.wantIdle("resetting a receive-only stream has no effect")
@@ -1277,7 +1319,7 @@
 	// "An endpoint that receives a STOP_SENDING frame MUST send a RESET_STREAM frame if
 	// the stream is in the "Ready" or "Send" state."
 	// https://www.rfc-editor.org/rfc/rfc9000#section-3.5-4
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc, s := newTestConnAndLocalStream(t, serverSide, styp, permissiveTransportParameters)
 		for i := 0; i < 4; i++ {
 			s.Write([]byte{byte(i)})
@@ -1309,6 +1351,9 @@
 }
 
 func TestStreamReceiveDataBlocked(t *testing.T) {
+	synctest.Test(t, testStreamReceiveDataBlocked)
+}
+func testStreamReceiveDataBlocked(t *testing.T) {
 	tc := newTestConn(t, serverSide, permissiveTransportParameters)
 	tc.handshake()
 	tc.ignoreFrame(frameTypeAck)
@@ -1326,7 +1371,7 @@
 }
 
 func TestStreamFlushExplicit(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		tc, s := newTestConnAndLocalStream(t, clientSide, styp, permissiveTransportParameters)
 		want := []byte{0, 1, 2, 3}
 		n, err := s.Write(want)
@@ -1344,6 +1389,9 @@
 }
 
 func TestStreamFlushClosedStream(t *testing.T) {
+	synctest.Test(t, testStreamFlushClosedStream)
+}
+func testStreamFlushClosedStream(t *testing.T) {
 	_, s := newTestConnAndLocalStream(t, clientSide, bidiStream,
 		permissiveTransportParameters)
 	s.Close()
@@ -1353,6 +1401,9 @@
 }
 
 func TestStreamFlushResetStream(t *testing.T) {
+	synctest.Test(t, testStreamFlushResetStream)
+}
+func testStreamFlushResetStream(t *testing.T) {
 	_, s := newTestConnAndLocalStream(t, clientSide, bidiStream,
 		permissiveTransportParameters)
 	s.Reset(0)
@@ -1362,6 +1413,9 @@
 }
 
 func TestStreamFlushStreamAfterPeerStopSending(t *testing.T) {
+	synctest.Test(t, testStreamFlushStreamAfterPeerStopSending)
+}
+func testStreamFlushStreamAfterPeerStopSending(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, clientSide, bidiStream,
 		permissiveTransportParameters)
 	s.Flush() // create the stream
@@ -1381,6 +1435,9 @@
 }
 
 func TestStreamErrorsAfterConnectionClosed(t *testing.T) {
+	synctest.Test(t, testStreamErrorsAfterConnectionClosed)
+}
+func testStreamErrorsAfterConnectionClosed(t *testing.T) {
 	tc, s := newTestConnAndLocalStream(t, clientSide, bidiStream,
 		permissiveTransportParameters)
 	wantErr := &ApplicationError{Code: 42}
@@ -1399,7 +1456,7 @@
 }
 
 func TestStreamFlushImplicitExact(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		const writeBufferSize = 4
 		tc, s := newTestConnAndLocalStream(t, clientSide, styp,
 			permissiveTransportParameters,
@@ -1429,7 +1486,7 @@
 }
 
 func TestStreamFlushImplicitLargerThanBuffer(t *testing.T) {
-	testStreamTypes(t, "", func(t *testing.T, styp streamType) {
+	testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) {
 		const writeBufferSize = 4
 		tc, s := newTestConnAndLocalStream(t, clientSide, styp,
 			permissiveTransportParameters,
diff --git a/quic/tls_test.go b/quic/tls_test.go
index 08c75dd..0818c68 100644
--- a/quic/tls_test.go
+++ b/quic/tls_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -9,12 +11,12 @@
 	"crypto/x509"
 	"errors"
 	"testing"
+	"testing/synctest"
 	"time"
 )
 
 // handshake executes the handshake.
 func (tc *testConn) handshake() {
-	tc.t.Helper()
 	if *testVV {
 		*testVV = false
 		defer func() {
@@ -32,16 +34,16 @@
 	i := 0
 	for {
 		if i == len(dgrams)-1 {
-			want := tc.endpoint.now.Add(maxAckDelay - timerGranularity)
+			want := time.Now().Add(maxAckDelay - timerGranularity)
 			if tc.conn.side == clientSide {
-				if !tc.timer.Equal(want) {
-					t.Fatalf("want timer = %v (max_ack_delay), got %v", want, tc.timer)
+				if got := tc.nextEvent(); !got.Equal(want) {
+					t.Fatalf("want timer = %v (max_ack_delay), got %v", want, got)
 				}
 				if got := tc.readDatagram(); got != nil {
 					t.Fatalf("client unexpectedly sent: %v", got)
 				}
 			}
-			tc.advanceTo(want)
+			time.Sleep(time.Until(want))
 		}
 
 		// Check that we're sending exactly the data we expect.
@@ -308,20 +310,29 @@
 }
 
 func TestConnClientHandshake(t *testing.T) {
+	synctest.Test(t, testConnClientHandshake)
+}
+func testConnClientHandshake(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	tc.handshake()
-	tc.advance(1 * time.Second)
+	time.Sleep(1 * time.Second)
 	tc.wantIdle("no packets should be sent by an idle conn after the handshake")
 }
 
 func TestConnServerHandshake(t *testing.T) {
+	synctest.Test(t, testConnServerHandshake)
+}
+func testConnServerHandshake(t *testing.T) {
 	tc := newTestConn(t, serverSide)
 	tc.handshake()
-	tc.advance(1 * time.Second)
+	time.Sleep(1 * time.Second)
 	tc.wantIdle("no packets should be sent by an idle conn after the handshake")
 }
 
 func TestConnKeysDiscardedClient(t *testing.T) {
+	synctest.Test(t, testConnKeysDiscardedClient)
+}
+func testConnKeysDiscardedClient(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	tc.ignoreFrame(frameTypeAck)
 
@@ -370,6 +381,9 @@
 }
 
 func TestConnKeysDiscardedServer(t *testing.T) {
+	synctest.Test(t, testConnKeysDiscardedServer)
+}
+func testConnKeysDiscardedServer(t *testing.T) {
 	tc := newTestConn(t, serverSide)
 	tc.ignoreFrame(frameTypeAck)
 
@@ -425,6 +439,9 @@
 }
 
 func TestConnInvalidCryptoData(t *testing.T) {
+	synctest.Test(t, testConnInvalidCryptoData)
+}
+func testConnInvalidCryptoData(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	tc.ignoreFrame(frameTypeAck)
 
@@ -455,6 +472,9 @@
 }
 
 func TestConnInvalidPeerCertificate(t *testing.T) {
+	synctest.Test(t, testConnInvalidPeerCertificate)
+}
+func testConnInvalidPeerCertificate(t *testing.T) {
 	tc := newTestConn(t, clientSide, func(c *tls.Config) {
 		c.VerifyPeerCertificate = func([][]byte, [][]*x509.Certificate) error {
 			return errors.New("I will not buy this certificate. It is scratched.")
@@ -481,6 +501,9 @@
 }
 
 func TestConnHandshakeDoneSentToServer(t *testing.T) {
+	synctest.Test(t, testConnHandshakeDoneSentToServer)
+}
+func testConnHandshakeDoneSentToServer(t *testing.T) {
 	tc := newTestConn(t, serverSide)
 	tc.handshake()
 
@@ -493,6 +516,9 @@
 }
 
 func TestConnCryptoDataOutOfOrder(t *testing.T) {
+	synctest.Test(t, testConnCryptoDataOutOfOrder)
+}
+func testConnCryptoDataOutOfOrder(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	tc.ignoreFrame(frameTypeAck)
 
@@ -531,6 +557,9 @@
 }
 
 func TestConnCryptoBufferSizeExceeded(t *testing.T) {
+	synctest.Test(t, testConnCryptoBufferSizeExceeded)
+}
+func testConnCryptoBufferSizeExceeded(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	tc.ignoreFrame(frameTypeAck)
 
@@ -550,6 +579,9 @@
 }
 
 func TestConnAEADLimitReached(t *testing.T) {
+	synctest.Test(t, testConnAEADLimitReached)
+}
+func testConnAEADLimitReached(t *testing.T) {
 	// "[...] endpoints MUST count the number of received packets that
 	// fail authentication during the lifetime of a connection.
 	// If the total number of received packets that fail authentication [...]
@@ -590,7 +622,7 @@
 		tc.conn.sendMsg(&datagram{
 			b: invalid,
 		})
-		tc.wait()
+		synctest.Wait()
 	}
 
 	// Set the conn's auth failure count to just before the AEAD integrity limit.
@@ -610,11 +642,14 @@
 		})
 
 	tc.writeFrames(packetType1RTT, debugFramePing{})
-	tc.advance(1 * time.Second)
+	time.Sleep(1 * time.Second)
 	tc.wantIdle("auth failures at limit: conn does not process additional packets")
 }
 
 func TestConnKeysDiscardedWithExcessCryptoData(t *testing.T) {
+	synctest.Test(t, testConnKeysDiscardedWithExcessCryptoData)
+}
+func testConnKeysDiscardedWithExcessCryptoData(t *testing.T) {
 	tc := newTestConn(t, serverSide, permissiveTransportParameters)
 	tc.ignoreFrame(frameTypeAck)
 	tc.ignoreFrame(frameTypeNewConnectionID)
diff --git a/quic/version_test.go b/quic/version_test.go
index 60d8307..ac054a8 100644
--- a/quic/version_test.go
+++ b/quic/version_test.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.25
+
 package quic
 
 import (
@@ -9,9 +11,13 @@
 	"context"
 	"crypto/tls"
 	"testing"
+	"testing/synctest"
 )
 
 func TestVersionNegotiationServerReceivesUnknownVersion(t *testing.T) {
+	synctest.Test(t, testVersionNegotiationServerReceivesUnknownVersion)
+}
+func testVersionNegotiationServerReceivesUnknownVersion(t *testing.T) {
 	config := &Config{
 		TLSConfig: newTestTLSConfig(serverSide),
 	}
@@ -55,6 +61,9 @@
 }
 
 func TestVersionNegotiationClientAborts(t *testing.T) {
+	synctest.Test(t, testVersionNegotiationClientAborts)
+}
+func testVersionNegotiationClientAborts(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	p := tc.readPacket() // client Initial packet
 	tc.endpoint.write(&datagram{
@@ -67,6 +76,9 @@
 }
 
 func TestVersionNegotiationClientIgnoresAfterProcessingPacket(t *testing.T) {
+	synctest.Test(t, testVersionNegotiationClientIgnoresAfterProcessingPacket)
+}
+func testVersionNegotiationClientIgnoresAfterProcessingPacket(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	tc.ignoreFrame(frameTypeAck)
 	p := tc.readPacket() // client Initial packet
@@ -89,6 +101,9 @@
 }
 
 func TestVersionNegotiationClientIgnoresMismatchingSourceConnID(t *testing.T) {
+	synctest.Test(t, testVersionNegotiationClientIgnoresMismatchingSourceConnID)
+}
+func testVersionNegotiationClientIgnoresMismatchingSourceConnID(t *testing.T) {
 	tc := newTestConn(t, clientSide)
 	tc.ignoreFrame(frameTypeAck)
 	p := tc.readPacket() // client Initial packet