From 3a3169551bf6b579fd8e5044479abf6bbfc3bbea Mon Sep 17 00:00:00 2001
From: Marten Seemann <martenseemann@gmail.com>
Date: Sun, 30 Jul 2023 16:31:30 -0400
Subject: [PATCH] detect kernel GSO support

---
 send_conn.go                |  4 +++-
 send_conn_test.go           |  6 +++---
 sys_conn_helper_darwin.go   |  3 +++
 sys_conn_helper_freebsd.go  |  3 +++
 sys_conn_helper_linux.go    | 12 ++++++++++++
 sys_conn_helper_nonlinux.go |  4 ++--
 sys_conn_oob.go             |  5 ++++-
 7 files changed, 30 insertions(+), 7 deletions(-)

diff --git a/send_conn.go b/send_conn.go
index 34cbfd6e..d8ddbc87 100644
--- a/send_conn.go
+++ b/send_conn.go
@@ -97,7 +97,9 @@ func (c *sconn) Write(p []byte, size protocol.ByteCount) error {
 
 func (c *sconn) capabilities() connCapabilities {
 	capabilities := c.rawConn.capabilities()
-	capabilities.GSO = !c.gotGSOError
+	if capabilities.GSO {
+		capabilities.GSO = !c.gotGSOError
+	}
 	return capabilities
 }
 
diff --git a/send_conn_test.go b/send_conn_test.go
index 8676d409..024a8eba 100644
--- a/send_conn_test.go
+++ b/send_conn_test.go
@@ -60,8 +60,8 @@ var _ = Describe("Connection (for sending packets)", func() {
 					Expect(oob).To(Equal(msg))
 					return 0, errGSO
 				}),
-				rawConn.EXPECT().WritePacket([]byte("foo"), remoteAddr, []byte{}).Return(3, nil),
-				rawConn.EXPECT().WritePacket([]byte("bar"), remoteAddr, []byte{}).Return(3, nil),
+				rawConn.EXPECT().WritePacket([]byte("foo"), remoteAddr, gomock.Len(0)).Return(3, nil),
+				rawConn.EXPECT().WritePacket([]byte("bar"), remoteAddr, gomock.Len(0)).Return(3, nil),
 			)
 			Expect(c.Write([]byte("foobar"), 3)).To(Succeed())
 			Expect(c.capabilities().GSO).To(BeFalse()) // GSO support is now disabled
@@ -75,7 +75,7 @@ var _ = Describe("Connection (for sending packets)", func() {
 			rawConn.EXPECT().LocalAddr()
 			rawConn.EXPECT().capabilities()
 			c := newSendConn(rawConn, remoteAddr, packetInfo{}, utils.DefaultLogger)
-			rawConn.EXPECT().WritePacket([]byte("foobar"), remoteAddr, nil)
+			rawConn.EXPECT().WritePacket([]byte("foobar"), remoteAddr, gomock.Len(0))
 			Expect(c.Write([]byte("foobar"), 6)).To(Succeed())
 		})
 	}
diff --git a/sys_conn_helper_darwin.go b/sys_conn_helper_darwin.go
index bf735f0f..758cf778 100644
--- a/sys_conn_helper_darwin.go
+++ b/sys_conn_helper_darwin.go
@@ -5,6 +5,7 @@ package quic
 import (
 	"encoding/binary"
 	"net/netip"
+	"syscall"
 
 	"golang.org/x/sys/unix"
 )
@@ -29,3 +30,5 @@ func parseIPv4PktInfo(body []byte) (ip netip.Addr, ifIndex uint32, ok bool) {
 	}
 	return netip.AddrFrom4(*(*[4]byte)(body[8:12])), binary.LittleEndian.Uint32(body), true
 }
+
+func isGSOSupported(syscall.RawConn) bool { return false }
diff --git a/sys_conn_helper_freebsd.go b/sys_conn_helper_freebsd.go
index fe5a7c20..a2baae3b 100644
--- a/sys_conn_helper_freebsd.go
+++ b/sys_conn_helper_freebsd.go
@@ -4,6 +4,7 @@ package quic
 
 import (
 	"net/netip"
+	"syscall"
 
 	"golang.org/x/sys/unix"
 )
@@ -24,3 +25,5 @@ func parseIPv4PktInfo(body []byte) (ip netip.Addr, _ uint32, ok bool) {
 	}
 	return netip.AddrFrom4(*(*[4]byte)(body)), 0, true
 }
+
+func isGSOSupported(syscall.RawConn) bool { return false }
diff --git a/sys_conn_helper_linux.go b/sys_conn_helper_linux.go
index 4e87bba0..5a177c29 100644
--- a/sys_conn_helper_linux.go
+++ b/sys_conn_helper_linux.go
@@ -52,6 +52,18 @@ func parseIPv4PktInfo(body []byte) (ip netip.Addr, ifIndex uint32, ok bool) {
 	return netip.AddrFrom4(*(*[4]byte)(body[8:12])), binary.LittleEndian.Uint32(body), true
 }
 
+// isGSOSupported tests if the kernel supports GSO.
+// Sending with GSO might still fail later on, if the interface doesn't support it (see isGSOError).
+func isGSOSupported(conn syscall.RawConn) bool {
+	var serr error
+	if err := conn.Control(func(fd uintptr) {
+		_, serr = unix.GetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_SEGMENT)
+	}); err != nil {
+		return false
+	}
+	return serr == nil
+}
+
 func appendUDPSegmentSizeMsg(b []byte, size uint16) []byte {
 	startLen := len(b)
 	const dataLen = 2 // payload is a uint16
diff --git a/sys_conn_helper_nonlinux.go b/sys_conn_helper_nonlinux.go
index 48ab10aa..cace82d5 100644
--- a/sys_conn_helper_nonlinux.go
+++ b/sys_conn_helper_nonlinux.go
@@ -5,5 +5,5 @@ package quic
 func forceSetReceiveBuffer(c any, bytes int) error { return nil }
 func forceSetSendBuffer(c any, bytes int) error    { return nil }
 
-func appendUDPSegmentSizeMsg(_ []byte, _ uint16) []byte { return nil }
-func isGSOError(error) bool                             { return false }
+func appendUDPSegmentSizeMsg([]byte, uint16) []byte { return nil }
+func isGSOError(error) bool                         { return false }
diff --git a/sys_conn_oob.go b/sys_conn_oob.go
index aa69262b..4026a7b3 100644
--- a/sys_conn_oob.go
+++ b/sys_conn_oob.go
@@ -137,8 +137,11 @@ func newConn(c OOBCapablePacketConn, supportsDF bool) (*oobConn, error) {
 		batchConn:            bc,
 		messages:             msgs,
 		readPos:              batchSize,
+		cap: connCapabilities{
+			DF:  supportsDF,
+			GSO: isGSOSupported(rawConn),
+		},
 	}
-	oobConn.cap.DF = supportsDF
 	for i := 0; i < batchSize; i++ {
 		oobConn.messages[i].OOB = make([]byte, oobBufferSize)
 	}