net/{batching,udprelay},wgengine/magicsock: add SO_RXQ_OVFL clientmetrics
For the purpose of improved observability of UDP socket receive buffer overflows on Linux. Updates tailscale/corp#37679 Signed-off-by: Jordan Whited <jordan@tailscale.com>
This commit is contained in:
committed by
Jordan Whited
parent
660a4608d2
commit
96dde53b43
+180
-25
@@ -5,10 +5,13 @@ package batching
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/tailscale/wireguard-go/conn"
|
||||
"golang.org/x/net/ipv6"
|
||||
"golang.org/x/sys/unix"
|
||||
@@ -317,35 +320,187 @@ func TestMinReadBatchMsgsLen(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getGSOSizeFromControl_MultipleMessages(t *testing.T) {
|
||||
// Test that getGSOSizeFromControl correctly parses UDP_GRO when it's not the first control message.
|
||||
const expectedGSOSize = 1420
|
||||
func makeControlMsg(cmsgLevel, cmsgType int32, dataLen int) []byte {
|
||||
msgLen := unix.CmsgSpace(dataLen)
|
||||
msg := make([]byte, msgLen)
|
||||
hdr2 := (*unix.Cmsghdr)(unsafe.Pointer(&msg[0]))
|
||||
hdr2.Level = cmsgLevel
|
||||
hdr2.Type = cmsgType
|
||||
hdr2.SetLen(unix.CmsgLen(dataLen))
|
||||
return msg
|
||||
}
|
||||
|
||||
// First message: IP_TOS
|
||||
firstMsgLen := unix.CmsgSpace(1)
|
||||
firstMsg := make([]byte, firstMsgLen)
|
||||
hdr1 := (*unix.Cmsghdr)(unsafe.Pointer(&firstMsg[0]))
|
||||
hdr1.Level = unix.SOL_IP
|
||||
hdr1.Type = unix.IP_TOS
|
||||
hdr1.SetLen(unix.CmsgLen(1))
|
||||
firstMsg[unix.SizeofCmsghdr] = 0
|
||||
func gsoControl(gso uint16) []byte {
|
||||
msg := makeControlMsg(unix.SOL_UDP, unix.UDP_GRO, 2)
|
||||
binary.NativeEndian.PutUint16(msg[unix.SizeofCmsghdr:], gso)
|
||||
return msg
|
||||
}
|
||||
|
||||
// Second message: UDP_GRO
|
||||
secondMsgLen := unix.CmsgSpace(2)
|
||||
secondMsg := make([]byte, secondMsgLen)
|
||||
hdr2 := (*unix.Cmsghdr)(unsafe.Pointer(&secondMsg[0]))
|
||||
hdr2.Level = unix.SOL_UDP
|
||||
hdr2.Type = unix.UDP_GRO
|
||||
hdr2.SetLen(unix.CmsgLen(2))
|
||||
binary.NativeEndian.PutUint16(secondMsg[unix.SizeofCmsghdr:], expectedGSOSize)
|
||||
func rxqOverflowsControl(count uint32) []byte {
|
||||
msg := makeControlMsg(unix.SOL_SOCKET, unix.SO_RXQ_OVFL, 4)
|
||||
binary.NativeEndian.PutUint32(msg[unix.SizeofCmsghdr:], count)
|
||||
return msg
|
||||
}
|
||||
|
||||
control := append(firstMsg, secondMsg...)
|
||||
func Test_getRXQOverflowsMetric(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
m := getRXQOverflowsMetric("")
|
||||
c.Assert(m, qt.IsNil)
|
||||
m = getRXQOverflowsMetric("rxq_overflows")
|
||||
c.Assert(m, qt.IsNotNil)
|
||||
wantM := getRXQOverflowsMetric("rxq_overflows")
|
||||
c.Assert(m, qt.Equals, wantM)
|
||||
uniq := getRXQOverflowsMetric("rxq_overflows_uniq")
|
||||
c.Assert(m, qt.Not(qt.Equals), uniq)
|
||||
}
|
||||
|
||||
gsoSize, err := getGSOSizeFromControl(control)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
func Test_getRXQOverflowsFromControl(t *testing.T) {
|
||||
malformedControlMsg := gsoControl(1)
|
||||
hdr := (*unix.Cmsghdr)(unsafe.Pointer(&malformedControlMsg[0]))
|
||||
hdr.SetLen(1)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
control []byte
|
||||
want uint32
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "malformed",
|
||||
control: malformedControlMsg,
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "gso",
|
||||
control: gsoControl(1),
|
||||
want: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "rxq overflows",
|
||||
control: rxqOverflowsControl(1),
|
||||
want: 1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple cmsg rxq overflows at head",
|
||||
control: append(rxqOverflowsControl(1), gsoControl(1)...),
|
||||
want: 1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple cmsg rxq overflows at tail",
|
||||
control: append(gsoControl(1), rxqOverflowsControl(1)...),
|
||||
want: 1,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
if gsoSize != expectedGSOSize {
|
||||
t.Errorf("got GSO size %d, want %d", gsoSize, expectedGSOSize)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := getRXQOverflowsFromControl(tt.control)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getRXQOverflowsFromControl() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("getRXQOverflowsFromControl() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getGSOSizeFromControl(t *testing.T) {
|
||||
malformedControlMsg := gsoControl(1)
|
||||
hdr := (*unix.Cmsghdr)(unsafe.Pointer(&malformedControlMsg[0]))
|
||||
hdr.SetLen(1)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
control []byte
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "malformed",
|
||||
control: malformedControlMsg,
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "gso",
|
||||
control: gsoControl(1),
|
||||
want: 1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "rxq overflows",
|
||||
control: rxqOverflowsControl(1),
|
||||
want: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple cmsg gso at tail",
|
||||
control: append(rxqOverflowsControl(1), gsoControl(1)...),
|
||||
want: 1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple cmsg gso at head",
|
||||
control: append(gsoControl(1), rxqOverflowsControl(1)...),
|
||||
want: 1,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := getGSOSizeFromControl(tt.control)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getGSOSizeFromControl() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("getGSOSizeFromControl() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_linuxBatchingConn_handleRXQOverflowCounter(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
conn := &linuxBatchingConn{
|
||||
rxqOverflowsMetric: getRXQOverflowsMetric("test_handleRXQOverflowCounter"),
|
||||
}
|
||||
conn.rxqOverflowsMetric.Set(0) // test count > 1 will accumulate, reset
|
||||
|
||||
// n == 0
|
||||
conn.handleRXQOverflowCounter([]ipv6.Message{{}}, 0, nil)
|
||||
c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(0))
|
||||
|
||||
// rxErr non-nil
|
||||
conn.handleRXQOverflowCounter([]ipv6.Message{{}}, 0, io.EOF)
|
||||
c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(0))
|
||||
|
||||
// nonzero counter
|
||||
control := rxqOverflowsControl(1)
|
||||
conn.handleRXQOverflowCounter([]ipv6.Message{{
|
||||
OOB: control,
|
||||
NN: len(control),
|
||||
}}, 1, nil)
|
||||
c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(1))
|
||||
|
||||
// nonzero counter, no change
|
||||
conn.handleRXQOverflowCounter([]ipv6.Message{{
|
||||
OOB: control,
|
||||
NN: len(control),
|
||||
}}, 1, nil)
|
||||
c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(1))
|
||||
|
||||
// counter rollover
|
||||
control = rxqOverflowsControl(0)
|
||||
conn.handleRXQOverflowCounter([]ipv6.Message{{
|
||||
OOB: control,
|
||||
NN: len(control),
|
||||
}}, 1, nil)
|
||||
c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(1+math.MaxUint32))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user