diff --git a/send_conn.go b/send_conn.go index 4fda1469122..498ed112b46 100644 --- a/send_conn.go +++ b/send_conn.go @@ -28,6 +28,9 @@ type sconn struct { packetInfoOOB []byte // If GSO enabled, and we receive a GSO error for this remote address, GSO is disabled. gotGSOError bool + // Used to catch the error sometimes returned by the first sendmsg call on Linux, + // see https://github.com/golang/go/issues/63322. + wroteFirstPacket bool } var _ sendConn = &sconn{} @@ -56,7 +59,7 @@ func newSendConn(c rawConn, remote net.Addr, info packetInfo, logger utils.Logge } func (c *sconn) Write(p []byte, gsoSize uint16, ecn protocol.ECN) error { - _, err := c.WritePacket(p, c.remoteAddr, c.packetInfoOOB, gsoSize, ecn) + err := c.writePacket(p, c.remoteAddr, c.packetInfoOOB, gsoSize, ecn) if err != nil && isGSOError(err) { // disable GSO for future calls c.gotGSOError = true @@ -69,7 +72,7 @@ func (c *sconn) Write(p []byte, gsoSize uint16, ecn protocol.ECN) error { if l > int(gsoSize) { l = int(gsoSize) } - if _, err := c.WritePacket(p[:l], c.remoteAddr, c.packetInfoOOB, 0, ecn); err != nil { + if err := c.writePacket(p[:l], c.remoteAddr, c.packetInfoOOB, 0, ecn); err != nil { return err } p = p[l:] @@ -79,6 +82,15 @@ func (c *sconn) Write(p []byte, gsoSize uint16, ecn protocol.ECN) error { return err } +func (c *sconn) writePacket(p []byte, addr net.Addr, oob []byte, gsoSize uint16, ecn protocol.ECN) error { + _, err := c.WritePacket(p, addr, oob, gsoSize, ecn) + if err != nil && !c.wroteFirstPacket && isPermissionError(err) { + _, err = c.WritePacket(p, addr, oob, gsoSize, ecn) + } + c.wroteFirstPacket = true + return err +} + func (c *sconn) capabilities() connCapabilities { capabilities := c.rawConn.capabilities() if capabilities.GSO { diff --git a/send_conn_test.go b/send_conn_test.go index bbac8fe7c3d..f69c77e7619 100644 --- a/send_conn_test.go +++ b/send_conn_test.go @@ -76,4 +76,27 @@ var _ = Describe("Connection (for sending packets)", func() { Expect(c.capabilities().GSO).To(BeFalse()) }) } + + if runtime.GOOS == "linux" { + It("doesn't fail if the very first sendmsg call fails", func() { + rawConn := NewMockRawConn(mockCtrl) + rawConn.EXPECT().LocalAddr() + rawConn.EXPECT().capabilities().AnyTimes() + c := newSendConn(rawConn, remoteAddr, packetInfo{}, utils.DefaultLogger) + gomock.InOrder( + rawConn.EXPECT().WritePacket([]byte("foobar"), remoteAddr, gomock.Any(), gomock.Any(), protocol.ECNCE).Return(0, errNotPermitted), + rawConn.EXPECT().WritePacket([]byte("foobar"), remoteAddr, gomock.Any(), uint16(0), protocol.ECNCE).Return(6, nil), + ) + Expect(c.Write([]byte("foobar"), 0, protocol.ECNCE)).To(Succeed()) + }) + + It("fails if the sendmsg calls fail multiple times", func() { + rawConn := NewMockRawConn(mockCtrl) + rawConn.EXPECT().LocalAddr() + rawConn.EXPECT().capabilities().AnyTimes() + c := newSendConn(rawConn, remoteAddr, packetInfo{}, utils.DefaultLogger) + rawConn.EXPECT().WritePacket([]byte("foobar"), remoteAddr, gomock.Any(), gomock.Any(), protocol.ECNCE).Return(0, errNotPermitted).Times(2) + Expect(c.Write([]byte("foobar"), 0, protocol.ECNCE)).To(MatchError(errNotPermitted)) + }) + } }) diff --git a/sys_conn_helper_linux.go b/sys_conn_helper_linux.go index 622f4e6f344..8e326af9817 100644 --- a/sys_conn_helper_linux.go +++ b/sys_conn_helper_linux.go @@ -97,3 +97,14 @@ func isGSOError(err error) bool { } return false } + +// The first sendmsg call on a new UDP socket sometimes errors on Linux. +// It's not clear why this happens. +// See https://github.com/golang/go/issues/63322. +func isPermissionError(err error) bool { + var serr *os.SyscallError + if errors.As(err, &serr) { + return serr.Syscall == "sendmsg" && serr.Err == unix.EPERM + } + return false +} diff --git a/sys_conn_helper_linux_test.go b/sys_conn_helper_linux_test.go index 4cf59abe22c..3385fc81d8a 100644 --- a/sys_conn_helper_linux_test.go +++ b/sys_conn_helper_linux_test.go @@ -13,7 +13,10 @@ import ( . "github.com/onsi/gomega" ) -var errGSO = &os.SyscallError{Err: unix.EIO} +var ( + errGSO = &os.SyscallError{Err: unix.EIO} + 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() { diff --git a/sys_conn_helper_nonlinux.go b/sys_conn_helper_nonlinux.go index cace82d5dcc..f8d69803b5a 100644 --- a/sys_conn_helper_nonlinux.go +++ b/sys_conn_helper_nonlinux.go @@ -7,3 +7,4 @@ func forceSetSendBuffer(c any, bytes int) error { return nil } func appendUDPSegmentSizeMsg([]byte, uint16) []byte { return nil } func isGSOError(error) bool { return false } +func isPermissionError(err error) bool { return false } diff --git a/sys_conn_helper_nonlinux_test.go b/sys_conn_helper_nonlinux_test.go index 29d42ad33ab..09671241863 100644 --- a/sys_conn_helper_nonlinux_test.go +++ b/sys_conn_helper_nonlinux_test.go @@ -4,4 +4,7 @@ package quic import "errors" -var errGSO = errors.New("fake GSO error") +var ( + errGSO = errors.New("fake GSO error") + errNotPermitted = errors.New("fake not permitted error") +)