Repository: quic-go/quic-go
Pull request #5051 (2025-04-19 06:54:42)

protocol: fix flaky TestVersionGreasing

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/quic-go/quic-go/pull/5051?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 84.07%. Comparing base [(`c60438c`)](https://app.codecov.io/gh/quic-go/quic-go/commit/c60438c528d8c8fe641abb517e343b49e4142c54?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) to head [(`75c1855`)](https://app.codecov.io/gh/quic-go/quic-go/commit/75c18559444dc4dab90de69dd6d88ee79d7fc897?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).

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


```diff
@@            Coverage Diff             @@
##           master    #5051      +/-   ##
==========================================
- Coverage   84.24%   84.07%   -0.16%     
==========================================
  Files         153      150       -3     
  Lines       17003    16658     -345     
==========================================
- Hits        14323    14005     -318     
+ Misses       2151     2136      -15     
+ Partials      529      517      -12     
```

</details>

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

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 1c250667..9533f0b4 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -17,12 +17,6 @@ linters:
   settings:
     depguard:
       rules:
-        random:
-          deny:
-            - pkg: "math/rand$"
-              desc: use math/rand/v2
-            - pkg: "golang.org/x/exp/rand"
-              desc: use math/rand/v2
         quicvarint:
           list-mode: strict
           files:
diff --git a/fuzzing/frames/cmd/corpus.go b/fuzzing/frames/cmd/corpus.go
index 16701e51..5baed928 100644
--- a/fuzzing/frames/cmd/corpus.go
+++ b/fuzzing/frames/cmd/corpus.go
@@ -1,11 +1,11 @@
 package main
 
 import (
-	"crypto/rand"
 	"log"
-	mrand "math/rand/v2"
 	"time"
 
+	"golang.org/x/exp/rand"
+
 	"github.com/quic-go/quic-go"
 	"github.com/quic-go/quic-go/fuzzing/internal/helper"
 	"github.com/quic-go/quic-go/internal/protocol"
@@ -21,15 +21,15 @@ func getRandomData(l int) []byte {
 }
 
 func getRandomNumber() uint64 {
-	switch 1 << uint8(mrand.IntN(3)) {
+	switch 1 << uint8(rand.Intn(3)) {
 	case 1:
-		return uint64(mrand.IntN(64))
+		return uint64(rand.Int63n(64))
 	case 2:
-		return uint64(mrand.IntN(16384))
+		return uint64(rand.Int63n(16384))
 	case 4:
-		return uint64(mrand.IntN(1073741824))
+		return uint64(rand.Int63n(1073741824))
 	case 8:
-		return uint64(mrand.IntN(4611686018427387904))
+		return uint64(rand.Int63n(4611686018427387904))
 	default:
 		panic("unexpected length")
 	}
@@ -39,14 +39,14 @@ func getRandomNumberLowerOrEqual(target uint64) uint64 {
 	if target == 0 {
 		return 0
 	}
-	return mrand.Uint64N(target)
+	return uint64(rand.Int63n(int64(target)))
 }
 
 // returns a *maximum* number of num ACK ranges
 func getAckRanges(num int) []wire.AckRange {
 	var ranges []wire.AckRange
 
-	prevSmallest := uint64(mrand.IntN(4611686018427387904))
+	prevSmallest := uint64(rand.Int63n(4611686018427387904))
 	for i := 0; i < num; i++ {
 		if prevSmallest <= 2 {
 			break
@@ -266,17 +266,17 @@ func main() {
 		frames := getFrames()
 
 		var b []byte
-		for j := 0; j < mrand.IntN(30)+2; j++ {
-			if mrand.IntN(10) == 0 { // write a PADDING frame
+		for j := 0; j < rand.Intn(30)+2; j++ {
+			if rand.Intn(10) == 0 { // write a PADDING frame
 				b = append(b, 0)
 			}
-			f := frames[mrand.IntN(len(frames))]
+			f := frames[rand.Intn(len(frames))]
 			var err error
 			b, err = f.Append(b, version)
 			if err != nil {
 				log.Fatal(err)
 			}
-			if mrand.IntN(10) == 0 { // write a PADDING frame
+			if rand.Intn(10) == 0 { // write a PADDING frame
 				b = append(b, 0)
 			}
 		}
diff --git a/fuzzing/handshake/fuzz.go b/fuzzing/handshake/fuzz.go
index 7363b97d..157e9bd0 100644
--- a/fuzzing/handshake/fuzz.go
+++ b/fuzzing/handshake/fuzz.go
@@ -11,7 +11,7 @@ import (
 	"io"
 	"log"
 	"math"
-	mrand "math/rand/v2"
+	mrand "math/rand"
 	"net"
 	"time"
 
@@ -120,13 +120,13 @@ func toEncryptionLevel(n uint8) protocol.EncryptionLevel {
 
 func getTransportParameters(seed uint8) *wire.TransportParameters {
 	const maxVarInt = math.MaxUint64 / 4
-	r := mrand.New(mrand.NewPCG(uint64(seed), uint64(seed)))
+	r := mrand.New(mrand.NewSource(int64(seed)))
 	return &wire.TransportParameters{
 		ActiveConnectionIDLimit:        2,
-		InitialMaxData:                 protocol.ByteCount(r.IntN(maxVarInt)),
-		InitialMaxStreamDataBidiLocal:  protocol.ByteCount(r.IntN(maxVarInt)),
-		InitialMaxStreamDataBidiRemote: protocol.ByteCount(r.IntN(maxVarInt)),
-		InitialMaxStreamDataUni:        protocol.ByteCount(r.IntN(maxVarInt)),
+		InitialMaxData:                 protocol.ByteCount(r.Int63n(maxVarInt)),
+		InitialMaxStreamDataBidiLocal:  protocol.ByteCount(r.Int63n(maxVarInt)),
+		InitialMaxStreamDataBidiRemote: protocol.ByteCount(r.Int63n(maxVarInt)),
+		InitialMaxStreamDataUni:        protocol.ByteCount(r.Int63n(maxVarInt)),
 	}
 }
 
diff --git a/fuzzing/header/cmd/corpus.go b/fuzzing/header/cmd/corpus.go
index d72afdf3..ba53306f 100644
--- a/fuzzing/header/cmd/corpus.go
+++ b/fuzzing/header/cmd/corpus.go
@@ -1,9 +1,9 @@
 package main
 
 import (
-	"crypto/rand"
 	"log"
-	mrand "math/rand/v2"
+
+	"golang.org/x/exp/rand"
 
 	"github.com/quic-go/quic-go/fuzzing/header"
 	"github.com/quic-go/quic-go/fuzzing/internal/helper"
@@ -22,7 +22,7 @@ func getRandomData(l int) []byte {
 func getVNP(src, dest protocol.ArbitraryLenConnectionID, numVersions int) []byte {
 	versions := make([]protocol.Version, numVersions)
 	for i := 0; i < numVersions; i++ {
-		versions[i] = protocol.Version(mrand.Uint32())
+		versions[i] = protocol.Version(rand.Uint32())
 	}
 	return wire.ComposeVersionNegotiation(src, dest, versions)
 }
@@ -33,20 +33,20 @@ func main() {
 			SrcConnectionID:  protocol.ParseConnectionID(getRandomData(3)),
 			DestConnectionID: protocol.ParseConnectionID(getRandomData(8)),
 			Type:             protocol.PacketTypeInitial,
-			Length:           protocol.ByteCount(mrand.IntN(1000)),
+			Length:           protocol.ByteCount(rand.Intn(1000)),
 			Version:          version,
 		},
 		{ // Initial without token, with zero-length src conn id
 			DestConnectionID: protocol.ParseConnectionID(getRandomData(8)),
 			Type:             protocol.PacketTypeInitial,
-			Length:           protocol.ByteCount(mrand.IntN(1000)),
+			Length:           protocol.ByteCount(rand.Intn(1000)),
 			Version:          version,
 		},
 		{ // Initial with Token
 			SrcConnectionID:  protocol.ParseConnectionID(getRandomData(10)),
 			DestConnectionID: protocol.ParseConnectionID(getRandomData(19)),
 			Type:             protocol.PacketTypeInitial,
-			Length:           protocol.ByteCount(mrand.IntN(1000)),
+			Length:           protocol.ByteCount(rand.Intn(1000)),
 			Version:          version,
 			Token:            getRandomData(25),
 		},
@@ -54,20 +54,20 @@ func main() {
 			SrcConnectionID:  protocol.ParseConnectionID(getRandomData(5)),
 			DestConnectionID: protocol.ParseConnectionID(getRandomData(10)),
 			Type:             protocol.PacketTypeHandshake,
-			Length:           protocol.ByteCount(mrand.IntN(1000)),
+			Length:           protocol.ByteCount(rand.Intn(1000)),
 			Version:          version,
 		},
 		{ // Handshake packet, with zero-length src conn id
 			DestConnectionID: protocol.ParseConnectionID(getRandomData(12)),
 			Type:             protocol.PacketTypeHandshake,
-			Length:           protocol.ByteCount(mrand.IntN(1000)),
+			Length:           protocol.ByteCount(rand.Intn(1000)),
 			Version:          version,
 		},
 		{ // 0-RTT packet
 			SrcConnectionID:  protocol.ParseConnectionID(getRandomData(8)),
 			DestConnectionID: protocol.ParseConnectionID(getRandomData(9)),
 			Type:             protocol.PacketType0RTT,
-			Length:           protocol.ByteCount(mrand.IntN(1000)),
+			Length:           protocol.ByteCount(rand.Intn(1000)),
 			Version:          version,
 		},
 		{ // Retry Packet, with empty orig dest conn id
@@ -82,8 +82,8 @@ func main() {
 	for _, h := range headers {
 		extHdr := &wire.ExtendedHeader{
 			Header:          h,
-			PacketNumberLen: protocol.PacketNumberLen(mrand.IntN(4) + 1),
-			PacketNumber:    protocol.PacketNumber(mrand.Uint64()),
+			PacketNumberLen: protocol.PacketNumberLen(rand.Intn(4) + 1),
+			PacketNumber:    protocol.PacketNumber(rand.Uint64()),
 		}
 		b, err := extHdr.Append(nil, version)
 		if err != nil {
diff --git a/fuzzing/transportparameters/cmd/corpus.go b/fuzzing/transportparameters/cmd/corpus.go
index d0b65d44..282e7d33 100644
--- a/fuzzing/transportparameters/cmd/corpus.go
+++ b/fuzzing/transportparameters/cmd/corpus.go
@@ -1,13 +1,13 @@
 package main
 
 import (
-	"crypto/rand"
 	"log"
 	"math"
-	mrand "math/rand/v2"
 	"net/netip"
 	"time"
 
+	"golang.org/x/exp/rand"
+
 	"github.com/quic-go/quic-go/fuzzing/internal/helper"
 	"github.com/quic-go/quic-go/fuzzing/transportparameters"
 	"github.com/quic-go/quic-go/internal/protocol"
@@ -21,8 +21,8 @@ func getRandomData(l int) []byte {
 }
 
 func getRandomValue() uint64 {
-	maxVals := []uint64{math.MaxUint8 / 4, math.MaxUint16 / 4, math.MaxUint32 / 4, math.MaxUint64 / 4}
-	return mrand.Uint64N(maxVals[mrand.IntN(4)])
+	maxVals := []int64{math.MaxUint8 / 4, math.MaxUint16 / 4, math.MaxUint32 / 4, math.MaxUint64 / 4}
+	return uint64(rand.Int63n(maxVals[int(rand.Int31n(4))]))
 }
 
 func main() {
@@ -41,22 +41,22 @@ func main() {
 			MaxIdleTimeout:                 time.Duration(getRandomValue()),
 			ActiveConnectionIDLimit:        getRandomValue() + 2,
 		}
-		if mrand.IntN(2) == 0 {
-			tp.OriginalDestinationConnectionID = protocol.ParseConnectionID(getRandomData(mrand.IntN(21)))
+		if rand.Int()%2 == 0 {
+			tp.OriginalDestinationConnectionID = protocol.ParseConnectionID(getRandomData(rand.Intn(21)))
 		}
-		if mrand.IntN(2) == 0 {
-			tp.InitialSourceConnectionID = protocol.ParseConnectionID(getRandomData(mrand.IntN(21)))
+		if rand.Int()%2 == 0 {
+			tp.InitialSourceConnectionID = protocol.ParseConnectionID(getRandomData(rand.Intn(21)))
 		}
-		if mrand.IntN(2) == 0 {
-			connID := protocol.ParseConnectionID(getRandomData(mrand.IntN(21)))
+		if rand.Int()%2 == 0 {
+			connID := protocol.ParseConnectionID(getRandomData(rand.Intn(21)))
 			tp.RetrySourceConnectionID = &connID
 		}
-		if mrand.IntN(2) == 0 {
+		if rand.Int()%2 == 0 {
 			var token protocol.StatelessResetToken
 			rand.Read(token[:])
 			tp.StatelessResetToken = &token
 		}
-		if mrand.IntN(2) == 0 {
+		if rand.Int()%2 == 0 {
 			var token protocol.StatelessResetToken
 			rand.Read(token[:])
 			var ip4 [4]byte
@@ -64,17 +64,17 @@ func main() {
 			var ip6 [16]byte
 			rand.Read(ip6[:])
 			tp.PreferredAddress = &wire.PreferredAddress{
-				IPv4:                netip.AddrPortFrom(netip.AddrFrom4(ip4), uint16(mrand.Int())),
-				IPv6:                netip.AddrPortFrom(netip.AddrFrom16(ip6), uint16(mrand.Int())),
-				ConnectionID:        protocol.ParseConnectionID(getRandomData(mrand.IntN(21))),
+				IPv4:                netip.AddrPortFrom(netip.AddrFrom4(ip4), uint16(rand.Int())),
+				IPv6:                netip.AddrPortFrom(netip.AddrFrom16(ip6), uint16(rand.Int())),
+				ConnectionID:        protocol.ParseConnectionID(getRandomData(rand.Intn(21))),
 				StatelessResetToken: token,
 			}
 		}
 
 		var data []byte
-		if mrand.Int()%2 == 0 {
+		if rand.Int()%2 == 0 {
 			pers := protocol.PerspectiveServer
-			if mrand.Int()%2 == 0 {
+			if rand.Int()%2 == 0 {
 				pers = protocol.PerspectiveClient
 			}
 			data = tp.Marshal(pers)
diff --git a/go.mod b/go.mod
index cc3c00ff..8c1b7063 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
 	github.com/stretchr/testify v1.9.0
 	go.uber.org/mock v0.5.0
 	golang.org/x/crypto v0.26.0
+	golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
 	golang.org/x/net v0.28.0
 	golang.org/x/sync v0.8.0
 	golang.org/x/sys v0.23.0
diff --git a/go.sum b/go.sum
index 24be4fc2..ab8b2299 100644
--- a/go.sum
+++ b/go.sum
@@ -149,6 +149,8 @@ golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
 golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
+golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
 golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=


Test patch:
diff --git a/frame_sorter_test.go b/frame_sorter_test.go
index 5a844bdf..13015e93 100644
--- a/frame_sorter_test.go
+++ b/frame_sorter_test.go
@@ -1,12 +1,12 @@
 package quic
 
 import (
-	rand "crypto/rand"
 	"fmt"
 	"math"
-	mrand "math/rand/v2"
 	"testing"
 
+	"golang.org/x/exp/rand"
+
 	"github.com/quic-go/quic-go/internal/protocol"
 
 	"github.com/stretchr/testify/require"
@@ -90,11 +90,9 @@ func TestFrameSorterSimpleCases(t *testing.T) {
 // in particular when overlapping stream data is received.
 // This also includes returning buffers that are no longer needed.
 func TestFrameSorterGapHandling(t *testing.T) {
-	random := mrand.NewChaCha8([32]byte{'f', 'o', 'o', 'b', 'a', 'r'})
-
 	getData := func(l protocol.ByteCount) []byte {
 		b := make([]byte, l)
-		random.Read(b)
+		rand.Read(b)
 		return b
 	}
 
@@ -1406,10 +1404,7 @@ func testFrameSorterRandomized(t *testing.T, dataLen protocol.ByteCount, injectD
 	const num = 1000
 
 	data := make([]byte, num*int(dataLen))
-	var seed [32]byte
-	rand.Read(seed[:])
-	random := mrand.NewChaCha8(seed)
-	random.Read(data)
+	rand.Read(data)
 
 	frames := make([]frame, num)
 	for i := 0; i < num; i++ {
@@ -1421,7 +1416,7 @@ func testFrameSorterRandomized(t *testing.T, dataLen protocol.ByteCount, injectD
 			data:   b,
 		}
 	}
-	mrand.Shuffle(len(frames), func(i, j int) { frames[i], frames[j] = frames[j], frames[i] })
+	rand.Shuffle(len(frames), func(i, j int) { frames[i], frames[j] = frames[j], frames[i] })
 
 	s := newFrameSorter()
 
@@ -1434,7 +1429,7 @@ func testFrameSorterRandomized(t *testing.T, dataLen protocol.ByteCount, injectD
 	if injectDuplicates {
 		for i := 0; i < num/10; i++ {
 			cb, tr := getFrameSorterTestCallback(t)
-			df := frames[mrand.IntN(len(frames))]
+			df := frames[rand.Intn(len(frames))]
 			require.NoError(t, s.Push(df.data, df.offset, cb))
 			callbacks = append(callbacks, tr)
 		}
@@ -1443,8 +1438,8 @@ func testFrameSorterRandomized(t *testing.T, dataLen protocol.ByteCount, injectD
 		finalOffset := num * dataLen
 		for i := 0; i < num/3; i++ {
 			cb, tr := getFrameSorterTestCallback(t)
-			startOffset := protocol.ByteCount(mrand.IntN(int(finalOffset)))
-			endOffset := startOffset + protocol.ByteCount(mrand.IntN(int(finalOffset-startOffset)))
+			startOffset := protocol.ByteCount(rand.Intn(int(finalOffset)))
+			endOffset := startOffset + protocol.ByteCount(rand.Intn(int(finalOffset-startOffset)))
 			require.NoError(t, s.Push(data[startOffset:endOffset], startOffset, cb))
 			callbacks = append(callbacks, tr)
 		}
diff --git a/framer_test.go b/framer_test.go
index 9e4bfe58..3fdf8ca7 100644
--- a/framer_test.go
+++ b/framer_test.go
@@ -2,11 +2,11 @@ package quic
 
 import (
 	"bytes"
-	"encoding/binary"
-	"math/rand/v2"
 	"testing"
 	"time"
 
+	"golang.org/x/exp/rand"
+
 	"github.com/quic-go/quic-go/internal/ackhandler"
 	"github.com/quic-go/quic-go/internal/flowcontrol"
 	"github.com/quic-go/quic-go/internal/protocol"
@@ -232,7 +232,7 @@ func TestFramerDetectsFramePathResponseDoS(t *testing.T) {
 	var pathResponses []*wire.PathResponseFrame
 	for i := 0; i < 2*maxPathResponses; i++ {
 		var f wire.PathResponseFrame
-		binary.BigEndian.PutUint64(f.Data[:], rand.Uint64())
+		rand.Read(f.Data[:])
 		pathResponses = append(pathResponses, &f)
 		framer.QueueControlFrame(&f)
 	}
diff --git a/integrationtests/gomodvendor/go.mod b/integrationtests/gomodvendor/go.mod
index 82224296..6c8f0313 100644
--- a/integrationtests/gomodvendor/go.mod
+++ b/integrationtests/gomodvendor/go.mod
@@ -14,6 +14,7 @@ require (
 	github.com/quic-go/qpack v0.5.1 // indirect
 	go.uber.org/mock v0.5.0 // indirect
 	golang.org/x/crypto v0.26.0 // indirect
+	golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
 	golang.org/x/mod v0.18.0 // indirect
 	golang.org/x/net v0.28.0 // indirect
 	golang.org/x/sync v0.8.0 // indirect
diff --git a/integrationtests/gomodvendor/go.sum b/integrationtests/gomodvendor/go.sum
index 1944d3a7..1b6e5fce 100644
--- a/integrationtests/gomodvendor/go.sum
+++ b/integrationtests/gomodvendor/go.sum
@@ -31,6 +31,8 @@ go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
 go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
 golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
 golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
+golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
+golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
 golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
 golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
diff --git a/integrationtests/self/cancelation_test.go b/integrationtests/self/cancelation_test.go
index 8310f471..519bc65e 100644
--- a/integrationtests/self/cancelation_test.go
+++ b/integrationtests/self/cancelation_test.go
@@ -6,7 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"math/rand/v2"
+	"math/rand"
 	"sync"
 	"sync/atomic"
 	"testing"
@@ -30,7 +30,7 @@ func TestStreamReadCancellation(t *testing.T) {
 
 	t.Run("after reading some data", func(t *testing.T) {
 		testStreamCancellation(t, func(str quic.ReceiveStream) error {
-			length := rand.IntN(len(PRData) - 1)
+			length := int(rand.Int31n(int32(len(PRData) - 1)))
 			if _, err := io.ReadAll(io.LimitReader(str, int64(length))); err != nil {
 				return fmt.Errorf("reading stream data failed: %w", err)
 			}
@@ -88,7 +88,7 @@ func TestStreamWriteCancellation(t *testing.T) {
 
 	t.Run("after writing some data", func(t *testing.T) {
 		testStreamCancellation(t, nil, func(str quic.SendStream) error {
-			length := rand.IntN(len(PRData) - 1)
+			length := int(rand.Int31n(int32(len(PRData) - 1)))
 			if _, err := str.Write(PRData[:length]); err != nil {
 				return fmt.Errorf("writing stream data failed: %w", err)
 			}
@@ -157,7 +157,7 @@ func TestStreamReadWriteCancellation(t *testing.T) {
 	t.Run("after writing some data", func(t *testing.T) {
 		testStreamCancellation(t,
 			func(str quic.ReceiveStream) error {
-				length := rand.IntN(len(PRData) - 1)
+				length := int(rand.Int31n(int32(len(PRData) - 1)))
 				if _, err := io.ReadAll(io.LimitReader(str, int64(length))); err != nil {
 					return fmt.Errorf("reading stream data failed: %w", err)
 				}
@@ -166,7 +166,7 @@ func TestStreamReadWriteCancellation(t *testing.T) {
 				return err
 			},
 			func(str quic.SendStream) error {
-				length := rand.IntN(len(PRData) - 1)
+				length := int(rand.Int31n(int32(len(PRData) - 1)))
 				if _, err := str.Write(PRData[:length]); err != nil {
 					return fmt.Errorf("writing stream data failed: %w", err)
 				}
@@ -214,7 +214,7 @@ func testStreamCancellation(
 	var numCancellations int
 	actions := make([]bool, numStreams)
 	for i := range actions {
-		actions[i] = rand.IntN(2) == 0
+		actions[i] = rand.Intn(2) == 0
 		if actions[i] {
 			numCancellations++
 		}
@@ -374,7 +374,7 @@ func TestCancelAcceptStream(t *testing.T) {
 	for numToAccept < numStreams {
 		ctx, cancel := context.WithCancel(context.Background())
 		// cancel accepting half of the streams
-		if rand.Int()%2 == 0 {
+		if rand.Int31()%2 == 0 {
 			cancel()
 		} else {
 			numToAccept++
@@ -530,15 +530,15 @@ func TestHeavyStreamCancellation(t *testing.T) {
 		str.SetDeadline(time.Now().Add(time.Second))
 		go func() {
 			defer wg.Done()
-			if rand.Int()%2 == 0 {
+			if rand.Int31()%2 == 0 {
 				io.ReadAll(str)
 			}
 		}()
 		go func() {
 			defer wg.Done()
-			if rand.Int()%2 == 0 {
+			if rand.Int31()%2 == 0 {
 				str.Write([]byte("foobar"))
-				if rand.Int()%2 == 0 {
+				if rand.Int31()%2 == 0 {
 					str.Close()
 				}
 			}
@@ -547,13 +547,13 @@ func TestHeavyStreamCancellation(t *testing.T) {
 			defer wg.Done()
 			// Make sure we at least send out *something* for the last stream,
 			// otherwise the peer might never receive this anything for this stream.
-			if rand.Int()%2 == 0 || str.StreamID() == 4*(maxIncomingStreams-1) {
+			if rand.Int31()%2 == 0 || str.StreamID() == 4*(maxIncomingStreams-1) {
 				str.CancelWrite(1234)
 			}
 		}()
 		go func() {
 			defer wg.Done()
-			if rand.Int()%2 == 0 {
+			if rand.Int31()%2 == 0 {
 				str.CancelRead(1234)
 			}
 		}()
diff --git a/integrationtests/self/conn_id_test.go b/integrationtests/self/conn_id_test.go
index e9e088d1..84d382b7 100644
--- a/integrationtests/self/conn_id_test.go
+++ b/integrationtests/self/conn_id_test.go
@@ -5,7 +5,7 @@ import (
 	"crypto/rand"
 	"fmt"
 	"io"
-	mrand "math/rand/v2"
+	mrand "math/rand"
 	"testing"
 	"time"
 
@@ -33,7 +33,7 @@ func (c *connIDGenerator) GenerateConnectionID() (quic.ConnectionID, error) {
 
 func (c *connIDGenerator) ConnectionIDLen() int { return c.Length }
 
-func randomConnIDLen() int { return 2 + mrand.IntN(19) }
+func randomConnIDLen() int { return 2 + int(mrand.Int31n(19)) }
 
 func TestConnectionIDsZeroLength(t *testing.T) {
 	testTransferWithConnectionIDs(t, randomConnIDLen(), 0, nil, nil)
diff --git a/integrationtests/self/handshake_drop_test.go b/integrationtests/self/handshake_drop_test.go
index 7d1d10b7..a1ebe08f 100644
--- a/integrationtests/self/handshake_drop_test.go
+++ b/integrationtests/self/handshake_drop_test.go
@@ -3,11 +3,10 @@ package self_test
 import (
 	"bytes"
 	"context"
-	"crypto/rand"
 	"crypto/tls"
 	"fmt"
 	"io"
-	mrand "math/rand/v2"
+	mrand "math/rand"
 	"net"
 	"sync"
 	"sync/atomic"
@@ -190,7 +189,7 @@ func dropCallbackDropOneThird(direction quicproxy.Direction) quicproxy.DropCallb
 	var mx sync.Mutex
 	var incoming, outgoing int
 	return func(d quicproxy.Direction, _, _ net.Addr, _ []byte) bool {
-		drop := mrand.IntN(3) == 0
+		drop := mrand.Int63n(int64(3)) == 0
 
 		mx.Lock()
 		defer mx.Unlock()
@@ -275,10 +274,10 @@ func TestPostQuantumClientHello(t *testing.T) {
 	t.Cleanup(func() { wire.AdditionalTransportParametersClient = origAdditionalTransportParametersClient })
 
 	b := make([]byte, 2500) // the ClientHello will now span across 3 packets
-	rand.Read(b)
+	mrand.New(mrand.NewSource(time.Now().UnixNano())).Read(b)
 	wire.AdditionalTransportParametersClient = map[uint64][]byte{
 		// Avoid random collisions with the greased transport parameters.
-		uint64(27+31*(1000+mrand.IntN(31))/31) % quicvarint.Max: b,
+		uint64(27+31*(1000+mrand.Int63()/31)) % quicvarint.Max: b,
 	}
 
 	ln, proxyPort := startDropTestListenerAndProxy(t, 10*time.Millisecond, 20*time.Second, dropCallbackDropOneThird(quicproxy.DirectionIncoming), false, false)
diff --git a/integrationtests/self/http_test.go b/integrationtests/self/http_test.go
index 928fde71..4eb740c1 100644
--- a/integrationtests/self/http_test.go
+++ b/integrationtests/self/http_test.go
@@ -9,7 +9,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	mrand "math/rand/v2"
+	mrand "math/rand"
 	"net"
 	"net/http"
 	"net/http/httptrace"
@@ -42,7 +42,7 @@ func randomString(length int) string {
 	const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
 	b := make([]byte, length)
 	for i := range b {
-		n := mrand.IntN(len(alphabet))
+		n := mrand.Intn(len(alphabet))
 		b[i] = alphabet[n]
 	}
 	return string(b)
diff --git a/integrationtests/self/mitm_test.go b/integrationtests/self/mitm_test.go
index 7a4e1fb3..660bc76e 100644
--- a/integrationtests/self/mitm_test.go
+++ b/integrationtests/self/mitm_test.go
@@ -2,17 +2,17 @@ package self_test
 
 import (
 	"context"
-	"crypto/rand"
 	"errors"
 	"io"
 	"math"
-	mrand "math/rand/v2"
 	"net"
 	"sync"
 	"sync/atomic"
 	"testing"
 	"time"
 
+	"golang.org/x/exp/rand"
+
 	"github.com/quic-go/quic-go"
 	quicproxy "github.com/quic-go/quic-go/integrationtests/tools/proxy"
 	"github.com/quic-go/quic-go/internal/protocol"
@@ -86,11 +86,11 @@ func testMITMInjectRandomPackets(t *testing.T, direction quicproxy.Direction) {
 					Type:             hdr.Type,
 					Version:          hdr.Version,
 				},
-				PacketNumber:    protocol.PacketNumber(mrand.Int32N(math.MaxInt32 / 4)),
-				PacketNumberLen: protocol.PacketNumberLen(mrand.IntN(4) + 1),
+				PacketNumber:    protocol.PacketNumber(rand.Int31n(math.MaxInt32 / 4)),
+				PacketNumberLen: protocol.PacketNumberLen(rand.Int31n(4) + 1),
 			}
-			payloadLen := mrand.IntN(100)
-			replyHdr.Length = protocol.ByteCount(mrand.IntN(payloadLen + 1))
+			payloadLen := rand.Int31n(100)
+			replyHdr.Length = protocol.ByteCount(rand.Int31n(payloadLen + 1))
 			data, err := replyHdr.Append(nil, hdr.Version)
 			if err != nil {
 				panic("failed to append header: " + err.Error())
@@ -108,11 +108,11 @@ func testMITMInjectRandomPackets(t *testing.T, direction quicproxy.Direction) {
 		if err != nil && !errors.Is(err, wire.ErrInvalidReservedBits) { // normally, ParseShortHeader is called after decrypting the header
 			panic("failed to parse short header: " + err.Error())
 		}
-		data, err := wire.AppendShortHeader(nil, connID, pn, pnLen, protocol.KeyPhaseBit(mrand.IntN(2)))
+		data, err := wire.AppendShortHeader(nil, connID, pn, pnLen, protocol.KeyPhaseBit(rand.Intn(2)))
 		if err != nil {
 			return nil
 		}
-		payloadLen := mrand.IntN(100)
+		payloadLen := rand.Int31n(100)
 		r := make([]byte, payloadLen)
 		rand.Read(r)
 		return append(data, r...)
@@ -128,7 +128,7 @@ func testMITMInjectRandomPackets(t *testing.T, direction quicproxy.Direction) {
 		go func() {
 			ticker := time.NewTicker(rtt / 10)
 			defer ticker.Stop()
-			for range 10 {
+			for i := 0; i < 10; i++ {
 				switch direction {
 				case quicproxy.DirectionIncoming:
 					clientTransport.WriteTo(createRandomPacketOfSameType(b), serverTransport.Conn.LocalAddr())
@@ -175,15 +175,15 @@ func testMITMCorruptPackets(t *testing.T, direction quicproxy.Direction) {
 		}
 		isLongHeaderPacket := wire.IsLongHeaderPacket(b[0])
 		// corrupt 20% of long header packets and 5% of short header packets
-		if isLongHeaderPacket && mrand.IntN(4) != 0 {
+		if isLongHeaderPacket && rand.Intn(4) != 0 {
 			return false
 		}
-		if !isLongHeaderPacket && mrand.IntN(20) != 0 {
+		if !isLongHeaderPacket && rand.Intn(20) != 0 {
 			return false
 		}
 		numCorrupted.Add(1)
-		pos := mrand.IntN(len(b))
-		b[pos] = byte(mrand.IntN(256))
+		pos := rand.Intn(len(b))
+		b[pos] = byte(rand.Intn(256))
 		switch direction {
 		case quicproxy.DirectionIncoming:
 			clientTransport.WriteTo(b, serverTransport.Conn.LocalAddr())
diff --git a/integrationtests/self/multiplex_test.go b/integrationtests/self/multiplex_test.go
index dea62792..9437d311 100644
--- a/integrationtests/self/multiplex_test.go
+++ b/integrationtests/self/multiplex_test.go
@@ -3,16 +3,16 @@ package self_test
 import (
 	"bytes"
 	"context"
-	"crypto/rand"
 	"errors"
 	"fmt"
 	"io"
-	mrand "math/rand/v2"
 	"net"
 	"runtime"
 	"testing"
 	"time"
 
+	"golang.org/x/exp/rand"
+
 	"github.com/quic-go/quic-go"
 
 	"github.com/stretchr/testify/require"
@@ -235,14 +235,11 @@ func TestMultiplexingNonQUICPackets(t *testing.T) {
 	errChanNonQUIC := make(chan error, 1)
 	sendNonQUICPacket := make(chan struct{}, 1)
 	go func() {
-		var seed [32]byte
-		rand.Read(seed[:])
-		random := mrand.NewChaCha8(seed)
 		defer close(errChanNonQUIC)
 		var sentPackets int
 		for range sendNonQUICPacket {
 			b := make([]byte, packetLen)
-			random.Read(b[1:]) // keep the first byte set to 0, so it's not classified as a QUIC packet
+			rand.Read(b[1:]) // keep the first byte set to 0, so it's not classified as a QUIC packet
 			_, err := tr1.WriteTo(b, tr2.Conn.LocalAddr())
 			// The first sendmsg call on a new UDP socket sometimes errors on Linux.
 			// It's not clear why this happens.
@@ -264,13 +261,9 @@ func TestMultiplexingNonQUICPackets(t *testing.T) {
 	go func() {
 		defer close(errChanQUIC)
 		defer serverStr.Close()
-
-		var seed [32]byte
-		rand.Read(seed[:])
-		random := mrand.NewChaCha8(seed)
 		for range sendQUICPacket {
 			b := make([]byte, 1024)
-			random.Read(b)
+			rand.Read(b)
 			if _, err := serverStr.Write(b); err != nil {
 				errChanQUIC <- err
 				return
diff --git a/integrationtests/self/self_test.go b/integrationtests/self/self_test.go
index e1ea1127..40479c67 100644
--- a/integrationtests/self/self_test.go
+++ b/integrationtests/self/self_test.go
@@ -8,7 +8,7 @@ import (
 	"flag"
 	"fmt"
 	"io"
-	"math/rand/v2"
+	"math/rand"
 	"net"
 	"os"
 	"runtime/pprof"
@@ -304,7 +304,7 @@ func (r *readerWithTimeout) Read(p []byte) (n int, err error) {
 }
 
 func randomDuration(min, max time.Duration) time.Duration {
-	return min + time.Duration(rand.IntN(int(max-min)))
+	return min + time.Duration(rand.Int63n(int64(max-min)))
 }
 
 // contains0RTTPacket says if a packet contains a 0-RTT long header packet.
diff --git a/integrationtests/self/timeout_test.go b/integrationtests/self/timeout_test.go
index a4fabb6f..e92d6358 100644
--- a/integrationtests/self/timeout_test.go
+++ b/integrationtests/self/timeout_test.go
@@ -6,7 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	mrand "math/rand/v2"
+	mrand "math/rand"
 	"net"
 	"runtime"
 	"sync/atomic"
@@ -372,14 +372,14 @@ func TestTimeoutAfterSendingPacket(t *testing.T) {
 type faultyConn struct {
 	net.PacketConn
 
-	MaxPackets int
+	MaxPackets int32
 	counter    atomic.Int32
 }
 
 func (c *faultyConn) ReadFrom(p []byte) (int, net.Addr, error) {
 	n, addr, err := c.PacketConn.ReadFrom(p)
 	counter := c.counter.Add(1)
-	if counter <= int32(c.MaxPackets) {
+	if counter <= c.MaxPackets {
 		return n, addr, err
 	}
 	return 0, nil, io.ErrClosedPipe
@@ -387,7 +387,7 @@ func (c *faultyConn) ReadFrom(p []byte) (int, net.Addr, error) {
 
 func (c *faultyConn) WriteTo(p []byte, addr net.Addr) (int, error) {
 	counter := c.counter.Add(1)
-	if counter <= int32(c.MaxPackets) {
+	if counter <= c.MaxPackets {
 		return c.PacketConn.WriteTo(p, addr)
 	}
 	return 0, io.ErrClosedPipe
@@ -437,7 +437,7 @@ func testFaultyPacketConn(t *testing.T, pers protocol.Perspective) {
 
 	var cconn net.PacketConn = newUDPConnLocalhost(t)
 	var sconn net.PacketConn = newUDPConnLocalhost(t)
-	maxPackets := mrand.IntN(25)
+	maxPackets := mrand.Int31n(25)
 	t.Logf("blocking %s's connection after %d packets", pers, maxPackets)
 	switch pers {
 	case protocol.PerspectiveClient:
diff --git a/integrationtests/self/tracer_test.go b/integrationtests/self/tracer_test.go
index f8c0c7c2..8f17ea58 100644
--- a/integrationtests/self/tracer_test.go
+++ b/integrationtests/self/tracer_test.go
@@ -6,7 +6,7 @@ import (
 	"context"
 	"fmt"
 	"io"
-	mrand "math/rand/v2"
+	mrand "math/rand"
 	"sync"
 	"testing"
 	"time"
diff --git a/internal/ackhandler/received_packet_history_test.go b/internal/ackhandler/received_packet_history_test.go
index b6f54167..a5ac9b3e 100644
--- a/internal/ackhandler/received_packet_history_test.go
+++ b/internal/ackhandler/received_packet_history_test.go
@@ -1,7 +1,7 @@
 package ackhandler
 
 import (
-	"math/rand/v2"
+	"math/rand"
 	"slices"
 	"testing"
 
@@ -174,7 +174,7 @@ func TestReceivedPacketHistoryRandomized(t *testing.T) {
 	hist := newReceivedPacketHistory()
 	packets := make(map[protocol.PacketNumber]int)
 	const num = 2 * protocol.MaxNumAckRanges
-	numLostPackets := rand.IntN(protocol.MaxNumAckRanges)
+	numLostPackets := rand.Intn(protocol.MaxNumAckRanges)
 	numRcvdPackets := num - numLostPackets
 
 	for i := 0; i < num; i++ {
@@ -182,7 +182,7 @@ func TestReceivedPacketHistoryRandomized(t *testing.T) {
 	}
 	lostPackets := make([]protocol.PacketNumber, 0, numLostPackets)
 	for len(lostPackets) < numLostPackets {
-		p := protocol.PacketNumber(rand.IntN(num))
+		p := protocol.PacketNumber(rand.Intn(num))
 		if _, ok := packets[p]; ok {
 			lostPackets = append(lostPackets, p)
 			delete(packets, p)
@@ -202,7 +202,7 @@ func TestReceivedPacketHistoryRandomized(t *testing.T) {
 		require.True(t, hist.ReceivedPacket(p))
 		// sometimes receive a duplicate
 		if i > 0 && rand.Int()%5 == 0 {
-			require.False(t, hist.ReceivedPacket(ordered[rand.IntN(i)]))
+			require.False(t, hist.ReceivedPacket(ordered[rand.Intn(i)]))
 		}
 	}
 	var counter int
diff --git a/internal/congestion/pacer_test.go b/internal/congestion/pacer_test.go
index a79c1d7d..cc3c8a2b 100644
--- a/internal/congestion/pacer_test.go
+++ b/internal/congestion/pacer_test.go
@@ -1,8 +1,7 @@
 package congestion
 
 import (
-	"math"
-	"math/rand/v2"
+	"math/rand"
 	"testing"
 	"time"
 
@@ -26,7 +25,7 @@ func TestPacerPacing(t *testing.T) {
 	}
 
 	// now packets are being paced
-	for range 5 {
+	for i := 0; i < 5; i++ {
 		require.Zero(t, p.Budget(now))
 		nextPacket := p.TimeUntilSend()
 		require.NotZero(t, nextPacket)
@@ -100,7 +99,7 @@ func TestPacerFastPacing(t *testing.T) {
 	require.Equal(t, 10*initialMaxDatagramSize, p.Budget(now.Add(time.Millisecond)))
 
 	now = now.Add(time.Millisecond)
-	for range 10 {
+	for i := 0; i < 10; i++ {
 		require.NotZero(t, p.Budget(now))
 		p.SentPacket(now, initialMaxDatagramSize)
 	}
@@ -112,7 +111,7 @@ func TestPacerNoOverflows(t *testing.T) {
 	p := newPacer(func() Bandwidth { return infBandwidth })
 	now := time.Now()
 	p.SentPacket(now, initialMaxDatagramSize)
-	for range 100000 {
-		require.NotZero(t, p.Budget(now.Add(time.Duration(rand.Int64N(math.MaxInt64)))))
+	for i := 0; i < 1e5; i++ {
+		require.NotZero(t, p.Budget(now.Add(time.Duration(rand.Int63()))))
 	}
 }
diff --git a/internal/handshake/initial_aead_test.go b/internal/handshake/initial_aead_test.go
index 3881c6ec..b1c1fbee 100644
--- a/internal/handshake/initial_aead_test.go
+++ b/internal/handshake/initial_aead_test.go
@@ -2,9 +2,10 @@ package handshake
 
 import (
 	"bytes"
-	"crypto/rand"
 	"testing"
 
+	"golang.org/x/exp/rand"
+
 	"github.com/quic-go/quic-go/internal/protocol"
 
 	"github.com/stretchr/testify/require"
@@ -256,10 +257,11 @@ func BenchmarkInitialAEAD(b *testing.B) {
 	clientSealer, _ := NewInitialAEAD(connectionID, protocol.PerspectiveClient, protocol.Version1)
 	_, serverOpener := NewInitialAEAD(connectionID, protocol.PerspectiveServer, protocol.Version1)
 
+	r := rand.New(rand.NewSource(1))
 	packetData := make([]byte, 1200)
-	rand.Read(packetData)
+	r.Read(packetData)
 	hdr := make([]byte, 50)
-	rand.Read(hdr)
+	r.Read(hdr)
 	msg := clientSealer.Seal(nil, packetData, 42, hdr)
 	m, err := serverOpener.Open(nil, msg, 42, hdr)
 	if err != nil {
@@ -269,7 +271,6 @@ func BenchmarkInitialAEAD(b *testing.B) {
 		b.Fatal("decrypted data doesn't match")
 	}
 
-	b.ResetTimer()
 	b.Run("opening 100 bytes", func(b *testing.B) {
 		benchmarkOpen(b, serverOpener, clientSealer.Seal(nil, packetData[:100], 42, hdr), hdr)
 	})
diff --git a/internal/protocol/version_test.go b/internal/protocol/version_test.go
index ec705f9e..f82137fd 100644
--- a/internal/protocol/version_test.go
+++ b/internal/protocol/version_test.go
@@ -102,18 +102,21 @@ func TestVersionGreasing(t *testing.T) {
 	require.Len(t, greased, 1)
 	require.True(t, isReservedVersion(greased[0]))
 
-	// make sure that the greased versions are distinct
+	// make sure that the greased versions are distinct,
+	// allowing for a small number of duplicates
 	var versions []Version
-	for range 20 {
+	for range 25 {
 		versions = GetGreasedVersions(versions)
 	}
 	slices.Sort(versions)
+	var numDuplicates int
 	for i, v := range versions {
 		require.True(t, isReservedVersion(v))
-		if i > 0 {
-			require.NotEqual(t, versions[i-1], v)
+		if i > 0 && versions[i-1] == v {
+			numDuplicates++
 		}
 	}
+	require.LessOrEqual(t, numDuplicates, 3)
 
 	// adding it somewhere in a slice of supported versions
 	supported := []Version{10, 18, 29}
diff --git a/internal/wire/frame_parser_test.go b/internal/wire/frame_parser_test.go
index 807170b5..f9568016 100644
--- a/internal/wire/frame_parser_test.go
+++ b/internal/wire/frame_parser_test.go
@@ -2,10 +2,11 @@ package wire
 
 import (
 	"bytes"
-	"crypto/rand"
 	"testing"
 	"time"
 
+	"golang.org/x/exp/rand"
+
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/qerr"
 
diff --git a/internal/wire/header_test.go b/internal/wire/header_test.go
index 326a778d..c7dd6274 100644
--- a/internal/wire/header_test.go
+++ b/internal/wire/header_test.go
@@ -5,8 +5,9 @@ import (
 	"crypto/rand"
 	"encoding/binary"
 	"io"
-	mrand "math/rand/v2"
+	mrand "math/rand"
 	"testing"
+	"time"
 
 	"github.com/quic-go/quic-go/internal/protocol"
 
@@ -107,8 +108,8 @@ func TestParseArbitraryLengthConnectionIDs(t *testing.T) {
 		return c
 	}
 
-	src := generateConnID(mrand.IntN(255) + 1)
-	dest := generateConnID(mrand.IntN(255) + 1)
+	src := generateConnID(mrand.Intn(255) + 1)
+	dest := generateConnID(mrand.Intn(255) + 1)
 	b := []byte{0x80, 1, 2, 3, 4}
 	b = append(b, uint8(dest.Len()))
 	b = append(b, dest.Bytes()...)
@@ -449,12 +450,11 @@ func TestPacketTypeForLogging(t *testing.T) {
 }
 
 func BenchmarkIs0RTTPacket(b *testing.B) {
-	src := mrand.NewChaCha8([32]byte{'f', 'o', 'o', 'b', 'a', 'r'})
-	random := mrand.New(src)
+	random := mrand.New(mrand.NewSource(time.Now().UnixNano()))
 	packets := make([][]byte, 1024)
 	for i := 0; i < len(packets); i++ {
-		packets[i] = make([]byte, random.IntN(256))
-		src.Read(packets[i])
+		packets[i] = make([]byte, random.Intn(256))
+		random.Read(packets[i])
 	}
 
 	b.ResetTimer()
diff --git a/internal/wire/transport_parameter_test.go b/internal/wire/transport_parameter_test.go
index 2970cdbc..d94b857d 100644
--- a/internal/wire/transport_parameter_test.go
+++ b/internal/wire/transport_parameter_test.go
@@ -2,14 +2,14 @@ package wire
 
 import (
 	"bytes"
-	"crypto/rand"
 	"fmt"
 	"math"
-	mrand "math/rand/v2"
 	"net/netip"
 	"testing"
 	"time"
 
+	"golang.org/x/exp/rand"
+
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/qerr"
 	"github.com/quic-go/quic-go/quicvarint"
@@ -17,9 +17,13 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func getRandomValueUpTo(max uint64) uint64 {
-	maxVals := []uint64{math.MaxUint8 / 4, math.MaxUint16 / 4, math.MaxUint32 / 4, math.MaxUint64 / 4}
-	return mrand.Uint64N(min(max, maxVals[mrand.IntN(4)]))
+func getRandomValueUpTo(max int64) uint64 {
+	maxVals := []int64{math.MaxUint8 / 4, math.MaxUint16 / 4, math.MaxUint32 / 4, math.MaxUint64 / 4}
+	m := maxVals[int(rand.Int31n(4))]
+	if m > max {
+		m = max
+	}
+	return uint64(rand.Int63n(m))
 }
 
 func getRandomValue() uint64 { return getRandomValueUpTo(quicvarint.Max) }
@@ -83,8 +87,8 @@ func TestMarshalAndUnmarshalTransportParameters(t *testing.T) {
 		InitialMaxStreamDataUni:         protocol.ByteCount(getRandomValue()),
 		InitialMaxData:                  protocol.ByteCount(getRandomValue()),
 		MaxIdleTimeout:                  0xcafe * time.Second,
-		MaxBidiStreamNum:                protocol.StreamNum(getRandomValueUpTo(uint64(protocol.MaxStreamCount))),
-		MaxUniStreamNum:                 protocol.StreamNum(getRandomValueUpTo(uint64(protocol.MaxStreamCount))),
+		MaxBidiStreamNum:                protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
+		MaxUniStreamNum:                 protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
 		DisableActiveMigration:          true,
 		StatelessResetToken:             &token,
 		OriginalDestinationConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
@@ -546,10 +550,10 @@ func TestTransportParametersFromSessionTicket(t *testing.T) {
 		InitialMaxStreamDataBidiRemote: protocol.ByteCount(getRandomValue()),
 		InitialMaxStreamDataUni:        protocol.ByteCount(getRandomValue()),
 		InitialMaxData:                 protocol.ByteCount(getRandomValue()),
-		MaxBidiStreamNum:               protocol.StreamNum(getRandomValueUpTo(uint64(protocol.MaxStreamCount))),
-		MaxUniStreamNum:                protocol.StreamNum(getRandomValueUpTo(uint64(protocol.MaxStreamCount))),
+		MaxBidiStreamNum:               protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
+		MaxUniStreamNum:                protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
 		ActiveConnectionIDLimit:        2 + getRandomValueUpTo(quicvarint.Max-2),
-		MaxDatagramFrameSize:           protocol.ByteCount(getRandomValueUpTo(uint64(MaxDatagramSize))),
+		MaxDatagramFrameSize:           protocol.ByteCount(getRandomValueUpTo(int64(MaxDatagramSize))),
 	}
 	require.True(t, params.ValidFor0RTT(params))
 	b := params.MarshalForSessionTicket(nil)
@@ -835,8 +839,8 @@ func benchmarkTransportParameters(b *testing.B, withPreferredAddress bool) {
 		InitialMaxStreamDataUni:         protocol.ByteCount(getRandomValue()),
 		InitialMaxData:                  protocol.ByteCount(getRandomValue()),
 		MaxIdleTimeout:                  0xcafe * time.Second,
-		MaxBidiStreamNum:                protocol.StreamNum(getRandomValueUpTo(uint64(protocol.MaxStreamCount))),
-		MaxUniStreamNum:                 protocol.StreamNum(getRandomValueUpTo(uint64(protocol.MaxStreamCount))),
+		MaxBidiStreamNum:                protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
+		MaxUniStreamNum:                 protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
 		DisableActiveMigration:          true,
 		StatelessResetToken:             &token,
 		OriginalDestinationConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
diff --git a/internal/wire/version_negotiation_test.go b/internal/wire/version_negotiation_test.go
index 3b44b2b6..f4af6312 100644
--- a/internal/wire/version_negotiation_test.go
+++ b/internal/wire/version_negotiation_test.go
@@ -1,11 +1,11 @@
 package wire
 
 import (
-	"crypto/rand"
 	"encoding/binary"
-	mrand "math/rand/v2"
 	"testing"
 
+	"golang.org/x/exp/rand"
+
 	"github.com/quic-go/quic-go/internal/protocol"
 
 	"github.com/stretchr/testify/require"
@@ -19,8 +19,8 @@ func TestParseVersionNegotiationPacket(t *testing.T) {
 		return b
 	}
 
-	srcConnID := randConnID(mrand.IntN(255) + 1)
-	destConnID := randConnID(mrand.IntN(255) + 1)
+	srcConnID := randConnID(rand.Intn(255) + 1)
+	destConnID := randConnID(rand.Intn(255) + 1)
 	versions := []protocol.Version{0x22334455, 0x33445566}
 	data := []byte{0x80, 0, 0, 0, 0}
 	data = append(data, uint8(len(destConnID)))
diff --git a/quicvarint/varint_test.go b/quicvarint/varint_test.go
index eef37033..065119e5 100644
--- a/quicvarint/varint_test.go
+++ b/quicvarint/varint_test.go
@@ -3,9 +3,10 @@ package quicvarint
 import (
 	"bytes"
 	"io"
-	"math/rand/v2"
 	"testing"
 
+	"golang.org/x/exp/rand"
+
 	"github.com/stretchr/testify/require"
 )
 
@@ -178,10 +179,10 @@ type benchmarkValue struct {
 }
 
 func randomValues(num int, maxValue uint64) []benchmarkValue {
-	r := rand.New(rand.NewPCG(13, 37))
+	r := rand.New(rand.NewSource(1))
 
 	bv := make([]benchmarkValue, num)
-	for i := range num {
+	for i := 0; i < num; i++ {
 		v := r.Uint64() % maxValue
 		bv[i].v = v
 		bv[i].b = Append([]byte{}, v)
diff --git a/send_stream_test.go b/send_stream_test.go
index 9588691e..f28b2d78 100644
--- a/send_stream_test.go
+++ b/send_stream_test.go
@@ -3,16 +3,17 @@ package quic
 import (
 	"bytes"
 	"context"
-	"crypto/rand"
 	"errors"
 	"fmt"
 	"io"
-	mrand "math/rand/v2"
+	mrand "math/rand"
 	"net"
 	"os"
 	"testing"
 	"time"
 
+	"golang.org/x/exp/rand"
+
 	"github.com/quic-go/quic-go/internal/mocks"
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/wire"
@@ -991,7 +992,7 @@ func TestSendStreamRetransmitDataUntilAcknowledged(t *testing.T) {
 
 	mockSender.EXPECT().onHasStreamData(streamID, str).AnyTimes()
 	mockFC.EXPECT().SendWindowSize().DoAndReturn(func() protocol.ByteCount {
-		return protocol.ByteCount(mrand.IntN(500)) + 50
+		return protocol.ByteCount(mrand.Intn(500)) + 50
 	}).AnyTimes()
 	mockFC.EXPECT().IsNewlyBlocked().Return(false).AnyTimes()
 	mockFC.EXPECT().AddBytesSent(gomock.Any()).AnyTimes()
@@ -1012,14 +1013,14 @@ func TestSendStreamRetransmitDataUntilAcknowledged(t *testing.T) {
 
 	received := make([]byte, dataLen)
 	for !completed {
-		f, _, _ := str.popStreamFrame(protocol.ByteCount(mrand.IntN(300)+100), protocol.Version1)
+		f, _, _ := str.popStreamFrame(protocol.ByteCount(mrand.Intn(300)+100), protocol.Version1)
 		if f.Frame == nil {
 			continue
 		}
 		sf := f.Frame
 		// 50%: acknowledge the frame and save the data
 		// 50%: lose the frame
-		if mrand.IntN(100) < 50 {
+		if mrand.Intn(100) < 50 {
 			copy(received[sf.Offset:sf.Offset+sf.DataLen()], sf.Data)
 			f.Handler.OnAcked(f.Frame)
 		} else {