Skip to content

Commit

Permalink
catch EPERM sendmsg errors for the very first packet on Linux
Browse files Browse the repository at this point in the history
  • Loading branch information
marten-seemann committed Oct 13, 2023
1 parent 49e588a commit 84ffb94
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 4 deletions.
16 changes: 14 additions & 2 deletions send_conn.go
Expand Up @@ -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{}
Expand Down Expand Up @@ -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
Expand All @@ -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:]
Expand All @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions send_conn_test.go
Expand Up @@ -76,4 +76,29 @@ 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)
Expect(c.capabilities().GSO).To(BeTrue())
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)
Expect(c.capabilities().GSO).To(BeTrue())
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))
})
}
})
11 changes: 11 additions & 0 deletions sys_conn_helper_linux.go
Expand Up @@ -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
}
5 changes: 4 additions & 1 deletion sys_conn_helper_linux_test.go
Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions sys_conn_helper_nonlinux.go
Expand Up @@ -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 }
5 changes: 4 additions & 1 deletion sys_conn_helper_nonlinux_test.go
Expand Up @@ -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")
)

0 comments on commit 84ffb94

Please sign in to comment.