Repository: quic-go/quic-go
Pull request #4797 (2024-12-24 10:06:24)

migrate platform-dependent UDP socket test code away from Ginkgo
Part of #3652.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/quic-go/quic-go/pull/4797?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 83.91%. Comparing base [(`484c0ba`)](https://app.codecov.io/gh/quic-go/quic-go/commit/484c0ba7b4b6bae69ad186d52862600f98474737?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) to head [(`6af6e32`)](https://app.codecov.io/gh/quic-go/quic-go/commit/6af6e32b0789043905b886a78fe8a74e9599e67d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).
> Report is 1 commits behind head on master.

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


```diff
@@            Coverage Diff             @@
##           master    #4797      +/-   ##
==========================================
- Coverage   83.99%   83.91%   -0.08%     
==========================================
  Files         149      146       -3     
  Lines       16058    15713     -345     
==========================================
- Hits        13487    13184     -303     
+ Misses       2042     2018      -24     
+ Partials      529      511      -18     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/quic-go/quic-go/pull/4797?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).


Patch:
diff --git a/mockgen.go b/mockgen.go
index 65ec465a..1a8b28db 100644
--- a/mockgen.go
+++ b/mockgen.go
@@ -61,9 +61,4 @@ type PacketHandler = packetHandler
 //go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package quic -self_package github.com/quic-go/quic-go -destination mock_packet_handler_manager_test.go github.com/quic-go/quic-go PacketHandlerManager"
 type PacketHandlerManager = packetHandlerManager
 
-// Need to use source mode for the batchConn, since reflect mode follows type aliases.
-// See https://github.com/golang/mock/issues/244 for details.
-//
-//go:generate sh -c "go run go.uber.org/mock/mockgen -typed -package quic -self_package github.com/quic-go/quic-go -source sys_conn_oob.go -destination mock_batch_conn_test.go -mock_names batchConn=MockBatchConn"
-
 //go:generate sh -c "go run go.uber.org/mock/mockgen -typed -package quic -self_package github.com/quic-go/quic-go -self_package github.com/quic-go/quic-go -destination mock_packetconn_test.go net PacketConn"
diff --git a/sys_conn_oob.go b/sys_conn_oob.go
index a6795ca2..75979682 100644
--- a/sys_conn_oob.go
+++ b/sys_conn_oob.go
@@ -83,7 +83,7 @@ func newConn(c OOBCapablePacketConn, supportsDF bool) (*oobConn, error) {
 	if err != nil {
 		return nil, err
 	}
-	needsPacketInfo := false
+	var needsPacketInfo bool
 	if udpAddr, ok := c.LocalAddr().(*net.UDPAddr); ok && udpAddr.IP.IsUnspecified() {
 		needsPacketInfo = true
 	}


Test patch:
diff --git a/mock_batch_conn_test.go b/mock_batch_conn_test.go
deleted file mode 100644
index 63f569c4..00000000
--- a/mock_batch_conn_test.go
+++ /dev/null
@@ -1,80 +0,0 @@
-// Code generated by MockGen. DO NOT EDIT.
-// Source: sys_conn_oob.go
-//
-// Generated by this command:
-//
-//	mockgen -typed -package quic -self_package github.com/quic-go/quic-go -source sys_conn_oob.go -destination mock_batch_conn_test.go -mock_names batchConn=MockBatchConn
-//
-
-// Package quic is a generated GoMock package.
-package quic
-
-import (
-	reflect "reflect"
-
-	gomock "go.uber.org/mock/gomock"
-	ipv4 "golang.org/x/net/ipv4"
-)
-
-// MockBatchConn is a mock of batchConn interface.
-type MockBatchConn struct {
-	ctrl     *gomock.Controller
-	recorder *MockBatchConnMockRecorder
-	isgomock struct{}
-}
-
-// MockBatchConnMockRecorder is the mock recorder for MockBatchConn.
-type MockBatchConnMockRecorder struct {
-	mock *MockBatchConn
-}
-
-// NewMockBatchConn creates a new mock instance.
-func NewMockBatchConn(ctrl *gomock.Controller) *MockBatchConn {
-	mock := &MockBatchConn{ctrl: ctrl}
-	mock.recorder = &MockBatchConnMockRecorder{mock}
-	return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockBatchConn) EXPECT() *MockBatchConnMockRecorder {
-	return m.recorder
-}
-
-// ReadBatch mocks base method.
-func (m *MockBatchConn) ReadBatch(ms []ipv4.Message, flags int) (int, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ReadBatch", ms, flags)
-	ret0, _ := ret[0].(int)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// ReadBatch indicates an expected call of ReadBatch.
-func (mr *MockBatchConnMockRecorder) ReadBatch(ms, flags any) *MockBatchConnReadBatchCall {
-	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadBatch", reflect.TypeOf((*MockBatchConn)(nil).ReadBatch), ms, flags)
-	return &MockBatchConnReadBatchCall{Call: call}
-}
-
-// MockBatchConnReadBatchCall wrap *gomock.Call
-type MockBatchConnReadBatchCall struct {
-	*gomock.Call
-}
-
-// Return rewrite *gomock.Call.Return
-func (c *MockBatchConnReadBatchCall) Return(arg0 int, arg1 error) *MockBatchConnReadBatchCall {
-	c.Call = c.Call.Return(arg0, arg1)
-	return c
-}
-
-// Do rewrite *gomock.Call.Do
-func (c *MockBatchConnReadBatchCall) Do(f func([]ipv4.Message, int) (int, error)) *MockBatchConnReadBatchCall {
-	c.Call = c.Call.Do(f)
-	return c
-}
-
-// DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockBatchConnReadBatchCall) DoAndReturn(f func([]ipv4.Message, int) (int, error)) *MockBatchConnReadBatchCall {
-	c.Call = c.Call.DoAndReturn(f)
-	return c
-}
diff --git a/sys_conn_helper_linux_test.go b/sys_conn_helper_linux_test.go
index 3385fc81..547d320b 100644
--- a/sys_conn_helper_linux_test.go
+++ b/sys_conn_helper_linux_test.go
@@ -6,11 +6,11 @@ import (
 	"errors"
 	"net"
 	"os"
+	"testing"
 
 	"golang.org/x/sys/unix"
 
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 )
 
 var (
@@ -18,64 +18,62 @@ var (
 	errNotPermitted = &os.SyscallError{Syscall: "sendmsg", Err: unix.EPERM}
 )
 
-var _ = Describe("forcing a change of send and receive buffer sizes", func() {
-	It("forces a change of the receive buffer size", func() {
-		if os.Getuid() != 0 {
-			Skip("Must be root to force change the receive buffer size")
-		}
-
-		c, err := net.ListenPacket("udp", "127.0.0.1:0")
-		Expect(err).ToNot(HaveOccurred())
-		defer c.Close()
-		syscallConn, err := c.(*net.UDPConn).SyscallConn()
-		Expect(err).ToNot(HaveOccurred())
-
-		const small = 256 << 10 // 256 KB
-		Expect(forceSetReceiveBuffer(syscallConn, small)).To(Succeed())
-
-		size, err := inspectReadBuffer(syscallConn)
-		Expect(err).ToNot(HaveOccurred())
-		//  The kernel doubles this value (to allow space for bookkeeping overhead)
-		Expect(size).To(Equal(2 * small))
-
-		const large = 32 << 20 // 32 MB
-		Expect(forceSetReceiveBuffer(syscallConn, large)).To(Succeed())
-		size, err = inspectReadBuffer(syscallConn)
-		Expect(err).ToNot(HaveOccurred())
-		//  The kernel doubles this value (to allow space for bookkeeping overhead)
-		Expect(size).To(Equal(2 * large))
-	})
-
-	It("forces a change of the send buffer size", func() {
-		if os.Getuid() != 0 {
-			Skip("Must be root to force change the send buffer size")
-		}
-
-		c, err := net.ListenPacket("udp", "127.0.0.1:0")
-		Expect(err).ToNot(HaveOccurred())
-		defer c.Close()
-		syscallConn, err := c.(*net.UDPConn).SyscallConn()
-		Expect(err).ToNot(HaveOccurred())
-
-		const small = 256 << 10 // 256 KB
-		Expect(forceSetSendBuffer(syscallConn, small)).To(Succeed())
-
-		size, err := inspectWriteBuffer(syscallConn)
-		Expect(err).ToNot(HaveOccurred())
-		//  The kernel doubles this value (to allow space for bookkeeping overhead)
-		Expect(size).To(Equal(2 * small))
-
-		const large = 32 << 20 // 32 MB
-		Expect(forceSetSendBuffer(syscallConn, large)).To(Succeed())
-		size, err = inspectWriteBuffer(syscallConn)
-		Expect(err).ToNot(HaveOccurred())
-		//  The kernel doubles this value (to allow space for bookkeeping overhead)
-		Expect(size).To(Equal(2 * large))
-	})
-
-	It("detects GSO errors", func() {
-		Expect(isGSOError(errGSO)).To(BeTrue())
-		Expect(isGSOError(nil)).To(BeFalse())
-		Expect(isGSOError(errors.New("test"))).To(BeFalse())
-	})
-})
+func TestForcingReceiveBufferSize(t *testing.T) {
+	if os.Getuid() != 0 {
+		t.Skip("Must be root to force change the receive buffer size")
+	}
+
+	c, err := net.ListenPacket("udp", "127.0.0.1:0")
+	require.NoError(t, err)
+	defer c.Close()
+	syscallConn, err := c.(*net.UDPConn).SyscallConn()
+	require.NoError(t, err)
+
+	const small = 256 << 10 // 256 KB
+	require.NoError(t, forceSetReceiveBuffer(syscallConn, small))
+
+	size, err := inspectReadBuffer(syscallConn)
+	require.NoError(t, err)
+	// the kernel doubles this value (to allow space for bookkeeping overhead)
+	require.Equal(t, 2*small, size)
+
+	const large = 32 << 20 // 32 MB
+	require.NoError(t, forceSetReceiveBuffer(syscallConn, large))
+	size, err = inspectReadBuffer(syscallConn)
+	require.NoError(t, err)
+	// the kernel doubles this value (to allow space for bookkeeping overhead)
+	require.Equal(t, 2*large, size)
+}
+
+func TestForcingSendBufferSize(t *testing.T) {
+	if os.Getuid() != 0 {
+		t.Skip("Must be root to force change the send buffer size")
+	}
+
+	c, err := net.ListenPacket("udp", "127.0.0.1:0")
+	require.NoError(t, err)
+	defer c.Close()
+	syscallConn, err := c.(*net.UDPConn).SyscallConn()
+	require.NoError(t, err)
+
+	const small = 256 << 10 // 256 KB
+	require.NoError(t, forceSetSendBuffer(syscallConn, small))
+
+	size, err := inspectWriteBuffer(syscallConn)
+	require.NoError(t, err)
+	// the kernel doubles this value (to allow space for bookkeeping overhead)
+	require.Equal(t, 2*small, size)
+
+	const large = 32 << 20 // 32 MB
+	require.NoError(t, forceSetSendBuffer(syscallConn, large))
+	size, err = inspectWriteBuffer(syscallConn)
+	require.NoError(t, err)
+	// the kernel doubles this value (to allow space for bookkeeping overhead)
+	require.Equal(t, 2*large, size)
+}
+
+func TestGSOError(t *testing.T) {
+	require.True(t, isGSOError(errGSO))
+	require.False(t, isGSOError(nil))
+	require.False(t, isGSOError(errors.New("test")))
+}
diff --git a/sys_conn_oob_test.go b/sys_conn_oob_test.go
index 7fe74eff..afedf5a1 100644
--- a/sys_conn_oob_test.go
+++ b/sys_conn_oob_test.go
@@ -5,6 +5,7 @@ package quic
 import (
 	"fmt"
 	"net"
+	"testing"
 	"time"
 
 	"golang.org/x/net/ipv4"
@@ -12,11 +13,255 @@ import (
 
 	"github.com/quic-go/quic-go/internal/protocol"
 
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-	"go.uber.org/mock/gomock"
+	"github.com/stretchr/testify/require"
 )
 
+func isIPv4(ip net.IP) bool { return ip.To4() != nil }
+
+func runSysConnServer(t *testing.T, network string, addr *net.UDPAddr) (*net.UDPAddr, <-chan receivedPacket) {
+	t.Helper()
+	udpConn, err := net.ListenUDP(network, addr)
+	require.NoError(t, err)
+	t.Cleanup(func() { udpConn.Close() })
+
+	oobConn, err := newConn(udpConn, true)
+	require.NoError(t, err)
+	require.True(t, oobConn.capabilities().DF)
+
+	packetChan := make(chan receivedPacket, 1)
+	go func() {
+		for {
+			p, err := oobConn.ReadPacket()
+			if err != nil {
+				return
+			}
+			packetChan <- p
+		}
+	}()
+	return udpConn.LocalAddr().(*net.UDPAddr), packetChan
+}
+
+// sendUDPPacketWithECN opens a new UDP socket and sends one packet with the ECN set.
+// It returns the local address of the socket.
+func sendUDPPacketWithECN(t *testing.T, network string, addr *net.UDPAddr, setECN func(uintptr)) net.Addr {
+	conn, err := net.DialUDP(network, nil, addr)
+	require.NoError(t, err)
+	t.Cleanup(func() { conn.Close() })
+
+	rawConn, err := conn.SyscallConn()
+	require.NoError(t, err)
+	require.NoError(t, rawConn.Control(func(fd uintptr) { setECN(fd) }))
+	_, err = conn.Write([]byte("foobar"))
+	require.NoError(t, err)
+	return conn.LocalAddr()
+}
+
+func TestReadECNFlagsIPv4(t *testing.T) {
+	addr, packetChan := runSysConnServer(t, "udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
+
+	sentFrom := sendUDPPacketWithECN(t,
+		"udp4",
+		addr,
+		func(fd uintptr) {
+			require.NoError(t, unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_TOS, 2))
+		},
+	)
+
+	select {
+	case p := <-packetChan:
+		require.WithinDuration(t, time.Now(), p.rcvTime, scaleDuration(20*time.Millisecond))
+		require.Equal(t, []byte("foobar"), p.data)
+		require.Equal(t, sentFrom, p.remoteAddr)
+		require.Equal(t, protocol.ECT0, p.ecn)
+	case <-time.After(time.Second):
+		t.Fatal("timeout waiting for packet")
+	}
+}
+
+func TestReadECNFlagsIPv6(t *testing.T) {
+	addr, packetChan := runSysConnServer(t, "udp6", &net.UDPAddr{IP: net.IPv6loopback, Port: 0})
+
+	sentFrom := sendUDPPacketWithECN(t,
+		"udp6",
+		addr,
+		func(fd uintptr) {
+			require.NoError(t, unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_TCLASS, 3))
+		},
+	)
+
+	select {
+	case p := <-packetChan:
+		require.WithinDuration(t, time.Now(), p.rcvTime, scaleDuration(20*time.Millisecond))
+		require.Equal(t, []byte("foobar"), p.data)
+		require.Equal(t, sentFrom, p.remoteAddr)
+		require.Equal(t, protocol.ECNCE, p.ecn)
+	case <-time.After(time.Second):
+		t.Fatal("timeout waiting for packet")
+	}
+}
+
+func TestReadECNFlagsDualStack(t *testing.T) {
+	addr, packetChan := runSysConnServer(t, "udp", &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 0})
+
+	// IPv4
+	sentFrom := sendUDPPacketWithECN(t,
+		"udp4",
+		&net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: addr.Port},
+		func(fd uintptr) {
+			require.NoError(t, unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_TOS, 3))
+		},
+	)
+
+	select {
+	case p := <-packetChan:
+		require.True(t, isIPv4(p.remoteAddr.(*net.UDPAddr).IP))
+		require.Equal(t, sentFrom.String(), p.remoteAddr.String())
+		require.Equal(t, protocol.ECNCE, p.ecn)
+	case <-time.After(time.Second):
+		t.Fatal("timeout waiting for packet")
+	}
+
+	// IPv6
+	sentFrom = sendUDPPacketWithECN(t,
+		"udp6",
+		&net.UDPAddr{IP: net.IPv6loopback, Port: addr.Port},
+		func(fd uintptr) {
+			require.NoError(t, unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_TCLASS, 1))
+		},
+	)
+
+	select {
+	case p := <-packetChan:
+		require.Equal(t, sentFrom, p.remoteAddr)
+		require.False(t, isIPv4(p.remoteAddr.(*net.UDPAddr).IP))
+		require.Equal(t, protocol.ECT1, p.ecn)
+	case <-time.After(time.Second):
+		t.Fatal("timeout waiting for packet")
+	}
+}
+
+func TestSendPacketsWithECNOnIPv4(t *testing.T) {
+	addr, packetChan := runSysConnServer(t, "udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
+
+	c, err := net.ListenUDP("udp4", nil)
+	require.NoError(t, err)
+	defer c.Close()
+
+	for _, val := range []protocol.ECN{protocol.ECNNon, protocol.ECT1, protocol.ECT0, protocol.ECNCE} {
+		_, _, err = c.WriteMsgUDP([]byte("foobar"), appendIPv4ECNMsg([]byte{}, val), addr)
+		require.NoError(t, err)
+		select {
+		case p := <-packetChan:
+			require.Equal(t, []byte("foobar"), p.data)
+			require.Equal(t, val, p.ecn)
+		case <-time.After(time.Second):
+			t.Fatal("timeout waiting for packet")
+		}
+	}
+}
+
+func TestSendPacketsWithECNOnIPv6(t *testing.T) {
+	addr, packetChan := runSysConnServer(t, "udp6", &net.UDPAddr{IP: net.IPv6loopback, Port: 0})
+
+	c, err := net.ListenUDP("udp6", nil)
+	require.NoError(t, err)
+	defer c.Close()
+
+	for _, val := range []protocol.ECN{protocol.ECNNon, protocol.ECT1, protocol.ECT0, protocol.ECNCE} {
+		_, _, err = c.WriteMsgUDP([]byte("foobar"), appendIPv6ECNMsg([]byte{}, val), addr)
+		require.NoError(t, err)
+		select {
+		case p := <-packetChan:
+			require.Equal(t, []byte("foobar"), p.data)
+			require.Equal(t, val, p.ecn)
+		case <-time.After(time.Second):
+			t.Fatal("timeout waiting for packet")
+		}
+	}
+}
+
+func TestSysConnPacketInfoIPv4(t *testing.T) {
+	// need to listen on 0.0.0.0, otherwise we won't get the packet info
+	addr, packetChan := runSysConnServer(t, "udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
+
+	conn, err := net.DialUDP("udp4", nil, addr)
+	require.NoError(t, err)
+	defer conn.Close()
+	_, err = conn.Write([]byte("foobar"))
+	require.NoError(t, err)
+
+	select {
+	case p := <-packetChan:
+		require.WithinDuration(t, time.Now(), p.rcvTime, scaleDuration(50*time.Millisecond))
+		require.Equal(t, []byte("foobar"), p.data)
+		require.Equal(t, conn.LocalAddr(), p.remoteAddr)
+		require.True(t, p.info.addr.IsValid())
+		require.True(t, isIPv4(p.info.addr.AsSlice()))
+		require.Equal(t, net.IPv4(127, 0, 0, 1).String(), p.info.addr.String())
+	case <-time.After(time.Second):
+		t.Fatal("timeout waiting for packet")
+	}
+}
+
+func TestSysConnPacketInfoIPv6(t *testing.T) {
+	// need to listen on ::, otherwise we won't get the packet info
+	addr, packetChan := runSysConnServer(t, "udp6", &net.UDPAddr{IP: net.IPv6zero, Port: 0})
+
+	conn, err := net.DialUDP("udp6", nil, addr)
+	require.NoError(t, err)
+	defer conn.Close()
+	_, err = conn.Write([]byte("foobar"))
+	require.NoError(t, err)
+
+	select {
+	case p := <-packetChan:
+		require.WithinDuration(t, time.Now(), p.rcvTime, scaleDuration(20*time.Millisecond))
+		require.Equal(t, []byte("foobar"), p.data)
+		require.Equal(t, conn.LocalAddr(), p.remoteAddr)
+		require.NotNil(t, p.info)
+		require.Equal(t, net.IPv6loopback, net.IP(p.info.addr.AsSlice()))
+	case <-time.After(time.Second):
+		t.Fatal("timeout waiting for packet")
+	}
+}
+
+func TestSysConnPacketInfoDualStack(t *testing.T) {
+	addr, packetChan := runSysConnServer(t, "udp", &net.UDPAddr{})
+
+	// IPv4
+	conn4, err := net.DialUDP("udp4", nil, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: addr.Port})
+	require.NoError(t, err)
+	defer conn4.Close()
+	_, err = conn4.Write([]byte("foobar"))
+	require.NoError(t, err)
+
+	select {
+	case p := <-packetChan:
+		require.True(t, isIPv4(p.remoteAddr.(*net.UDPAddr).IP))
+		require.NotNil(t, p.info)
+		require.True(t, p.info.addr.Is4())
+		require.Equal(t, net.IPv4(127, 0, 0, 1).String(), p.info.addr.String())
+	case <-time.After(time.Second):
+		t.Fatal("timeout waiting for IPv4 packet")
+	}
+
+	// IPv6
+	conn6, err := net.DialUDP("udp6", nil, addr)
+	require.NoError(t, err)
+	defer conn6.Close()
+	_, err = conn6.Write([]byte("foobar"))
+	require.NoError(t, err)
+
+	select {
+	case p := <-packetChan:
+		require.False(t, isIPv4(p.remoteAddr.(*net.UDPAddr).IP))
+		require.NotNil(t, p.info)
+		require.Equal(t, net.IPv6loopback.String(), p.info.addr.String())
+	case <-time.After(time.Second):
+		t.Fatal("timeout waiting for IPv6 packet")
+	}
+}
+
 type oobRecordingConn struct {
 	*net.UDPConn
 	oobs [][]byte
@@ -27,312 +272,63 @@ func (c *oobRecordingConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oob
 	return c.UDPConn.WriteMsgUDP(b, oob, addr)
 }
 
-func isIPv4(ip net.IP) bool { return ip.To4() != nil }
+type mockBatchConn struct {
+	t          *testing.T
+	numMsgRead int
 
-var _ = Describe("OOB Conn Test", func() {
-	runServer := func(network, address string) (*net.UDPConn, <-chan receivedPacket) {
-		addr, err := net.ResolveUDPAddr(network, address)
-		Expect(err).ToNot(HaveOccurred())
-		udpConn, err := net.ListenUDP(network, addr)
-		Expect(err).ToNot(HaveOccurred())
-		oobConn, err := newConn(udpConn, true)
-		Expect(err).ToNot(HaveOccurred())
-		Expect(oobConn.capabilities().DF).To(BeTrue())
-
-		packetChan := make(chan receivedPacket)
-		go func() {
-			defer GinkgoRecover()
-			for {
-				p, err := oobConn.ReadPacket()
-				if err != nil {
-					return
-				}
-				packetChan <- p
-			}
-		}()
+	callCounter int
+}
+
+var _ batchConn = &mockBatchConn{}
 
-		return udpConn, packetChan
+func (c *mockBatchConn) ReadBatch(ms []ipv4.Message, _ int) (int, error) {
+	require.Len(c.t, ms, batchSize)
+	for i := 0; i < c.numMsgRead; i++ {
+		require.Len(c.t, ms[i].Buffers, 1)
+		require.Len(c.t, ms[i].Buffers[0], protocol.MaxPacketBufferSize)
+		data := []byte(fmt.Sprintf("message %d", c.callCounter*c.numMsgRead+i))
+		ms[i].Buffers[0] = data
+		ms[i].N = len(data)
 	}
+	c.callCounter++
+	return c.numMsgRead, nil
+}
 
-	Context("reading ECN-marked packets", func() {
-		sendPacketWithECN := func(network string, addr *net.UDPAddr, setECN func(uintptr)) net.Addr {
-			conn, err := net.DialUDP(network, nil, addr)
-			ExpectWithOffset(1, err).ToNot(HaveOccurred())
-			rawConn, err := conn.SyscallConn()
-			ExpectWithOffset(1, err).ToNot(HaveOccurred())
-			ExpectWithOffset(1, rawConn.Control(func(fd uintptr) {
-				setECN(fd)
-			})).To(Succeed())
-			_, err = conn.Write([]byte("foobar"))
-			ExpectWithOffset(1, err).ToNot(HaveOccurred())
-			return conn.LocalAddr()
-		}
+func TestReadsMultipleMessagesInOneBatch(t *testing.T) {
+	bc := &mockBatchConn{t: t, numMsgRead: batchSize/2 + 1}
 
-		It("reads ECN flags on IPv4", func() {
-			conn, packetChan := runServer("udp4", "localhost:0")
-			defer conn.Close()
-
-			sentFrom := sendPacketWithECN(
-				"udp4",
-				conn.LocalAddr().(*net.UDPAddr),
-				func(fd uintptr) {
-					Expect(unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_TOS, 2)).To(Succeed())
-				},
-			)
-
-			var p receivedPacket
-			Eventually(packetChan).Should(Receive(&p))
-			Expect(p.rcvTime).To(BeTemporally("~", time.Now(), scaleDuration(20*time.Millisecond)))
-			Expect(p.data).To(Equal([]byte("foobar")))
-			Expect(p.remoteAddr).To(Equal(sentFrom))
-			Expect(p.ecn).To(Equal(protocol.ECT0))
-		})
-
-		It("reads ECN flags on IPv6", func() {
-			conn, packetChan := runServer("udp6", "[::]:0")
-			defer conn.Close()
-
-			sentFrom := sendPacketWithECN(
-				"udp6",
-				conn.LocalAddr().(*net.UDPAddr),
-				func(fd uintptr) {
-					Expect(unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_TCLASS, 3)).To(Succeed())
-				},
-			)
-
-			var p receivedPacket
-			Eventually(packetChan).Should(Receive(&p))
-			Expect(p.rcvTime).To(BeTemporally("~", time.Now(), scaleDuration(20*time.Millisecond)))
-			Expect(p.data).To(Equal([]byte("foobar")))
-			Expect(p.remoteAddr).To(Equal(sentFrom))
-			Expect(p.ecn).To(Equal(protocol.ECNCE))
-		})
-
-		It("reads ECN flags on a connection that supports both IPv4 and IPv6", func() {
-			conn, packetChan := runServer("udp", "0.0.0.0:0")
-			defer conn.Close()
-			port := conn.LocalAddr().(*net.UDPAddr).Port
-
-			// IPv4
-			sendPacketWithECN(
-				"udp4",
-				&net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port},
-				func(fd uintptr) {
-					Expect(unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_TOS, 3)).To(Succeed())
-				},
-			)
-
-			var p receivedPacket
-			Eventually(packetChan).Should(Receive(&p))
-			Expect(isIPv4(p.remoteAddr.(*net.UDPAddr).IP)).To(BeTrue())
-			Expect(p.ecn).To(Equal(protocol.ECNCE))
-
-			// IPv6
-			sendPacketWithECN(
-				"udp6",
-				&net.UDPAddr{IP: net.IPv6loopback, Port: port},
-				func(fd uintptr) {
-					Expect(unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_TCLASS, 1)).To(Succeed())
-				},
-			)
-
-			Eventually(packetChan).Should(Receive(&p))
-			Expect(isIPv4(p.remoteAddr.(*net.UDPAddr).IP)).To(BeFalse())
-			Expect(p.ecn).To(Equal(protocol.ECT1))
-		})
-
-		It("sends packets with ECN on IPv4", func() {
-			conn, packetChan := runServer("udp4", "localhost:0")
-			defer conn.Close()
-
-			c, err := net.ListenUDP("udp4", nil)
-			Expect(err).ToNot(HaveOccurred())
-			defer c.Close()
-
-			for _, val := range []protocol.ECN{protocol.ECNNon, protocol.ECT1, protocol.ECT0, protocol.ECNCE} {
-				_, _, err = c.WriteMsgUDP([]byte("foobar"), appendIPv4ECNMsg([]byte{}, val), conn.LocalAddr().(*net.UDPAddr))
-				Expect(err).ToNot(HaveOccurred())
-				var p receivedPacket
-				Eventually(packetChan).Should(Receive(&p))
-				Expect(p.data).To(Equal([]byte("foobar")))
-				Expect(p.ecn).To(Equal(val))
-			}
-		})
-
-		It("sends packets with ECN on IPv6", func() {
-			conn, packetChan := runServer("udp6", "[::1]:0")
-			defer conn.Close()
-
-			c, err := net.ListenUDP("udp6", nil)
-			Expect(err).ToNot(HaveOccurred())
-			defer c.Close()
-
-			for _, val := range []protocol.ECN{protocol.ECNNon, protocol.ECT1, protocol.ECT0, protocol.ECNCE} {
-				_, _, err = c.WriteMsgUDP([]byte("foobar"), appendIPv6ECNMsg([]byte{}, val), conn.LocalAddr().(*net.UDPAddr))
-				Expect(err).ToNot(HaveOccurred())
-				var p receivedPacket
-				Eventually(packetChan).Should(Receive(&p))
-				Expect(p.data).To(Equal([]byte("foobar")))
-				Expect(p.ecn).To(Equal(val))
-			}
-		})
-	})
-
-	Context("Packet Info conn", func() {
-		sendPacket := func(network string, addr *net.UDPAddr) net.Addr {
-			conn, err := net.DialUDP(network, nil, addr)
-			ExpectWithOffset(1, err).ToNot(HaveOccurred())
-			_, err = conn.Write([]byte("foobar"))
-			ExpectWithOffset(1, err).ToNot(HaveOccurred())
-			return conn.LocalAddr()
-		}
+	udpConn := newUPDConnLocalhost(t)
+	oobConn, err := newConn(udpConn, true)
+	require.NoError(t, err)
+	oobConn.batchConn = bc
 
-		It("reads packet info on IPv4", func() {
-			conn, packetChan := runServer("udp4", ":0")
-			defer conn.Close()
-
-			addr := conn.LocalAddr().(*net.UDPAddr)
-			ip := net.ParseIP("127.0.0.1").To4()
-			addr.IP = ip
-			sentFrom := sendPacket("udp4", addr)
-
-			var p receivedPacket
-			Eventually(packetChan).Should(Receive(&p))
-			Expect(p.rcvTime).To(BeTemporally("~", time.Now(), scaleDuration(20*time.Millisecond)))
-			Expect(p.data).To(Equal([]byte("foobar")))
-			Expect(p.remoteAddr).To(Equal(sentFrom))
-			Expect(p.info.addr.IsValid()).To(BeTrue())
-			Expect(net.IP(p.info.addr.AsSlice())).To(Equal(ip))
-		})
-
-		It("reads packet info on IPv6", func() {
-			conn, packetChan := runServer("udp6", ":0")
-			defer conn.Close()
-
-			addr := conn.LocalAddr().(*net.UDPAddr)
-			ip := net.ParseIP("::1")
-			addr.IP = ip
-			sentFrom := sendPacket("udp6", addr)
-
-			var p receivedPacket
-			Eventually(packetChan).Should(Receive(&p))
-			Expect(p.rcvTime).To(BeTemporally("~", time.Now(), scaleDuration(20*time.Millisecond)))
-			Expect(p.data).To(Equal([]byte("foobar")))
-			Expect(p.remoteAddr).To(Equal(sentFrom))
-			Expect(p.info).To(Not(BeNil()))
-			Expect(net.IP(p.info.addr.AsSlice())).To(Equal(ip))
-		})
-
-		It("reads packet info on a connection that supports both IPv4 and IPv6", func() {
-			conn, packetChan := runServer("udp", ":0")
-			defer conn.Close()
-			port := conn.LocalAddr().(*net.UDPAddr).Port
-
-			// IPv4
-			ip4 := net.ParseIP("127.0.0.1")
-			sendPacket("udp4", &net.UDPAddr{IP: ip4, Port: port})
-
-			var p receivedPacket
-			Eventually(packetChan).Should(Receive(&p))
-			Expect(isIPv4(p.remoteAddr.(*net.UDPAddr).IP)).To(BeTrue())
-			Expect(p.info).To(Not(BeNil()))
-			Expect(p.info.addr.Is4()).To(BeTrue())
-			ip := p.info.addr.As4()
-			Expect(net.IP(ip[:])).To(Equal(ip4.To4()))
-
-			// IPv6
-			ip6 := net.ParseIP("::1")
-			sendPacket("udp6", &net.UDPAddr{IP: net.IPv6loopback, Port: port})
-
-			Eventually(packetChan).Should(Receive(&p))
-			Expect(isIPv4(p.remoteAddr.(*net.UDPAddr).IP)).To(BeFalse())
-			Expect(p.info).To(Not(BeNil()))
-			Expect(net.IP(p.info.addr.AsSlice())).To(Equal(ip6))
-		})
-	})
-
-	Context("Batch Reading", func() {
-		var batchConn *MockBatchConn
-
-		BeforeEach(func() {
-			batchConn = NewMockBatchConn(mockCtrl)
-		})
-
-		It("reads multiple messages in one batch", func() {
-			const numMsgRead = batchSize/2 + 1
-			var counter int
-			batchConn.EXPECT().ReadBatch(gomock.Any(), gomock.Any()).DoAndReturn(func(ms []ipv4.Message, flags int) (int, error) {
-				Expect(ms).To(HaveLen(batchSize))
-				for i := 0; i < numMsgRead; i++ {
-					Expect(ms[i].Buffers).To(HaveLen(1))
-					Expect(ms[i].Buffers[0]).To(HaveLen(protocol.MaxPacketBufferSize))
-					data := []byte(fmt.Sprintf("message %d", counter))
-					counter++
-					ms[i].Buffers[0] = data
-					ms[i].N = len(data)
-				}
-				return numMsgRead, nil
-			}).Times(2)
-
-			addr, err := net.ResolveUDPAddr("udp", "localhost:0")
-			Expect(err).ToNot(HaveOccurred())
-			udpConn, err := net.ListenUDP("udp", addr)
-			Expect(err).ToNot(HaveOccurred())
-			oobConn, err := newConn(udpConn, true)
-			Expect(err).ToNot(HaveOccurred())
-			oobConn.batchConn = batchConn
-
-			for i := 0; i < batchSize+1; i++ {
-				p, err := oobConn.ReadPacket()
-				Expect(err).ToNot(HaveOccurred())
-				Expect(string(p.data)).To(Equal(fmt.Sprintf("message %d", i)))
-			}
-		})
-	})
-
-	Context("sending ECN-marked packets", func() {
-		It("sets the ECN control message", func() {
-			addr, err := net.ResolveUDPAddr("udp", "localhost:0")
-			Expect(err).ToNot(HaveOccurred())
-			udpConn, err := net.ListenUDP("udp", addr)
-			Expect(err).ToNot(HaveOccurred())
-			c := &oobRecordingConn{UDPConn: udpConn}
-			oobConn, err := newConn(c, true)
-			Expect(err).ToNot(HaveOccurred())
-
-			oob := make([]byte, 0, 123)
-			oobConn.WritePacket([]byte("foobar"), addr, oob, 0, protocol.ECNCE)
-			Expect(c.oobs).To(HaveLen(1))
-			oobMsg := c.oobs[0]
-			Expect(oobMsg).ToNot(BeEmpty())
-			Expect(oobMsg).To(HaveCap(cap(oob))) // check that it appended to oob
-			expected := appendIPv4ECNMsg([]byte{}, protocol.ECNCE)
-			Expect(oobMsg).To(Equal(expected))
-		})
-	})
-
-	if platformSupportsGSO {
-		Context("GSO", func() {
-			It("appends the GSO control message", func() {
-				addr, err := net.ResolveUDPAddr("udp", "localhost:0")
-				Expect(err).ToNot(HaveOccurred())
-				udpConn, err := net.ListenUDP("udp", addr)
-				Expect(err).ToNot(HaveOccurred())
-				c := &oobRecordingConn{UDPConn: udpConn}
-				oobConn, err := newConn(c, true)
-				Expect(err).ToNot(HaveOccurred())
-				Expect(oobConn.capabilities().GSO).To(BeTrue())
-
-				oob := make([]byte, 0, 123)
-				oobConn.WritePacket([]byte("foobar"), addr, oob, 3, protocol.ECNCE)
-				Expect(c.oobs).To(HaveLen(1))
-				oobMsg := c.oobs[0]
-				Expect(oobMsg).ToNot(BeEmpty())
-				Expect(oobMsg).To(HaveCap(cap(oob))) // check that it appended to oob
-				expected := appendUDPSegmentSizeMsg([]byte{}, 3)
-				// Check that the first control message is the OOB control message.
-				Expect(oobMsg[:len(expected)]).To(Equal(expected))
-			})
-		})
+	for i := 0; i < batchSize+1; i++ {
+		p, err := oobConn.ReadPacket()
+		require.NoError(t, err)
+		require.Equal(t, fmt.Sprintf("message %d", i), string(p.data))
+	}
+	require.Equal(t, 2, bc.callCounter)
+}
+
+func TestSysConnSendGSO(t *testing.T) {
+	if !platformSupportsGSO {
+		t.Skip("GSO not supported on this platform")
 	}
-})
+
+	udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
+	require.NoError(t, err)
+	c := &oobRecordingConn{UDPConn: udpConn}
+	oobConn, err := newConn(c, true)
+	require.NoError(t, err)
+	require.True(t, oobConn.capabilities().GSO)
+
+	oob := make([]byte, 0, 123)
+	oobConn.WritePacket([]byte("foobar"), udpConn.LocalAddr(), oob, 3, protocol.ECNCE)
+	require.Len(t, c.oobs, 1)
+	oobMsg := c.oobs[0]
+	require.NotEmpty(t, oobMsg)
+	require.Equal(t, cap(oob), cap(oobMsg)) // check that it appended to oob
+	expected := appendUDPSegmentSizeMsg([]byte{}, 3)
+	// Check that the first control message is the OOB control message.
+	require.Equal(t, expected, oobMsg[:len(expected)])
+}
diff --git a/sys_conn_test.go b/sys_conn_test.go
index 5eb0417a..d465c51b 100644
--- a/sys_conn_test.go
+++ b/sys_conn_test.go
@@ -2,31 +2,31 @@ package quic
 
 import (
 	"net"
+	"testing"
 	"time"
 
 	"github.com/quic-go/quic-go/internal/protocol"
 
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 	"go.uber.org/mock/gomock"
 )
 
-var _ = Describe("Basic Conn Test", func() {
-	It("reads a packet", func() {
-		c := NewMockPacketConn(mockCtrl)
-		addr := &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 1234}
-		c.EXPECT().ReadFrom(gomock.Any()).DoAndReturn(func(b []byte) (int, net.Addr, error) {
-			data := []byte("foobar")
-			Expect(b).To(HaveLen(protocol.MaxPacketBufferSize))
-			return copy(b, data), addr, nil
-		})
+func TestBasicConn(t *testing.T) {
+	mockCtrl := gomock.NewController(t)
 
-		conn, err := wrapConn(c)
-		Expect(err).ToNot(HaveOccurred())
-		p, err := conn.ReadPacket()
-		Expect(err).ToNot(HaveOccurred())
-		Expect(p.data).To(Equal([]byte("foobar")))
-		Expect(p.rcvTime).To(BeTemporally("~", time.Now(), scaleDuration(100*time.Millisecond)))
-		Expect(p.remoteAddr).To(Equal(addr))
+	c := NewMockPacketConn(mockCtrl)
+	addr := &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 1234}
+	c.EXPECT().ReadFrom(gomock.Any()).DoAndReturn(func(b []byte) (int, net.Addr, error) {
+		data := []byte("foobar")
+		require.Equal(t, protocol.MaxPacketBufferSize, len(b))
+		return copy(b, data), addr, nil
 	})
-})
+
+	conn, err := wrapConn(c)
+	require.NoError(t, err)
+	p, err := conn.ReadPacket()
+	require.NoError(t, err)
+	require.Equal(t, []byte("foobar"), p.data)
+	require.WithinDuration(t, time.Now(), p.rcvTime, scaleDuration(100*time.Millisecond))
+	require.Equal(t, addr, p.remoteAddr)
+}
diff --git a/sys_conn_windows_test.go b/sys_conn_windows_test.go
index 5da4a402..9376627f 100644
--- a/sys_conn_windows_test.go
+++ b/sys_conn_windows_test.go
@@ -4,31 +4,27 @@ package quic
 
 import (
 	"net"
+	"testing"
 
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 )
 
-var _ = Describe("Windows Conn Test", func() {
-	It("works on IPv4", func() {
-		addr, err := net.ResolveUDPAddr("udp4", "localhost:0")
-		Expect(err).ToNot(HaveOccurred())
-		udpConn, err := net.ListenUDP("udp4", addr)
-		Expect(err).ToNot(HaveOccurred())
+func TestWindowsConn(t *testing.T) {
+	t.Run("IPv4", func(t *testing.T) {
+		udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
+		require.NoError(t, err)
 		conn, err := newConn(udpConn, true)
-		Expect(err).ToNot(HaveOccurred())
-		Expect(conn.Close()).To(Succeed())
-		Expect(conn.capabilities().DF).To(BeTrue())
+		require.NoError(t, err)
+		require.NoError(t, conn.Close())
+		require.True(t, conn.capabilities().DF)
 	})
 
-	It("works on IPv6", func() {
-		addr, err := net.ResolveUDPAddr("udp6", "[::1]:0")
-		Expect(err).ToNot(HaveOccurred())
-		udpConn, err := net.ListenUDP("udp6", addr)
-		Expect(err).ToNot(HaveOccurred())
+	t.Run("IPv6", func(t *testing.T) {
+		udpConn, err := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6loopback, Port: 0})
+		require.NoError(t, err)
 		conn, err := newConn(udpConn, false)
-		Expect(err).ToNot(HaveOccurred())
-		Expect(conn.Close()).To(Succeed())
-		Expect(conn.capabilities().DF).To(BeFalse())
+		require.NoError(t, err)
+		require.NoError(t, conn.Close())
+		require.False(t, conn.capabilities().DF)
 	})
-})
+}