Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
package batching
import ( "encoding/binary" "errors" "io" "math" "net" "net/netip" "os" "sync" "sync/atomic" "testing" "unsafe"
qt "github.com/frankban/quicktest" "github.com/tailscale/wireguard-go/conn" "golang.org/x/net/ipv6" "golang.org/x/sys/unix" "tailscale.com/net/neterror" "tailscale.com/net/packet")
func Test_fillReceivedPackets(t *testing.T) { const maxDatagramSize = 1<<16 - 1
source := netip.MustParseAddrPort("192.0.2.1:1234") newMsg := func(n int, gso uint16) ipv6.Message { msg := ipv6.Message{ Buffers: [][]byte{make([]byte, maxDatagramSize)}, N: n, Addr: net.UDPAddrFromAddrPort(source), } if gso > 0 { msg.OOB = gsoControl(gso) msg.NN = len(msg.OOB) } return msg }
cases := []struct { name string msgs []ipv6.Message wantNumEval int wantPackets [][2]int // {offset, size} wantErr bool }{ { name: "first_split", msgs: []ipv6.Message{ newMsg(3, 1), }, wantNumEval: 3, wantPackets: [][2]int{ {0, 1}, {1, 1}, {2, 1}, }, wantErr: false, }, { name: "first_no_split", msgs: []ipv6.Message{ newMsg(1, 0), }, wantNumEval: 1, wantPackets: [][2]int{ {0, 1}, }, wantErr: false, }, { name: "first_no_split_last_no_split", msgs: []ipv6.Message{ newMsg(1, 0), newMsg(1, 0), }, wantNumEval: 2, wantPackets: [][2]int{ {0, 1}, {maxDatagramSize, 1}, }, wantErr: false, }, { name: "first_no_split_last_split", msgs: []ipv6.Message{ newMsg(1, 0), newMsg(3, 1), }, wantNumEval: 4, wantPackets: [][2]int{ {0, 1}, {maxDatagramSize, 1}, {maxDatagramSize + 1, 1}, {maxDatagramSize + 2, 1}, }, wantErr: false, }, { name: "first_split_last_split", msgs: []ipv6.Message{ newMsg(2, 1), newMsg(2, 1), }, wantNumEval: 4, wantPackets: [][2]int{ {0, 1}, {1, 1}, {maxDatagramSize, 1}, {maxDatagramSize + 1, 1}, }, wantErr: false, }, { name: "first_no_split_last_split_overflow", msgs: []ipv6.Message{ newMsg(1, 0), newMsg(4, 1), }, wantNumEval: 4, wantPackets: [][2]int{ {0, 1}, {maxDatagramSize, 1}, {maxDatagramSize + 1, 1}, {maxDatagramSize + 2, 1}, }, wantErr: true, }, { name: "split_with_short_tail", msgs: []ipv6.Message{ newMsg(5, 2), }, wantNumEval: 3, wantPackets: [][2]int{ {0, 2}, {2, 2}, {4, 1}, }, }, }
for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { packets := make([]ReceivedPacket, len(tt.wantPackets)) got, err := fillReceivedPackets( tt.msgs, maxDatagramSize, packets, true, ) if (err != nil) != tt.wantErr { t.Fatalf("err: %v, wantErr: %v", err, tt.wantErr) } if got != tt.wantNumEval { t.Fatalf("got to eval: %d want: %d", got, tt.wantNumEval) } if len(packets) != len(tt.wantPackets) { t.Fatalf("got %d packets, want %d", len(packets), len(tt.wantPackets)) } for i, want := range tt.wantPackets { got := packets[i] if got.Offset != want[0] || got.Size != want[1] || got.Source != source { t.Errorf( "packets[%d] = {Offset: %d, Size: %d, Source: %v}, want {Offset: %d, Size: %d, Source: %v}", i, got.Offset, got.Size, got.Source, want[0], want[1], source, ) } } }) }}
func Test_linuxBatchingConn_coalesceMessages(t *testing.T) { withGeneveSpace := func(len, cap int) []byte { return make([]byte, len+packet.GeneveFixedHeaderLength, cap+packet.GeneveFixedHeaderLength) }
geneve := packet.GeneveHeader{ Protocol: packet.GeneveProtocolWireGuard, } geneve.VNI.Set(1)
cases := []struct { name string buffs [][]byte geneve packet.GeneveHeader neverGSOEqualTail bool // Each wantLens slice corresponds to the Buffers of a single coalesced message, // and each int is the expected length of the corresponding Buffer[i]. wantLens [][]int wantGSO []int // wantSentinelAtTail[i], when true, asserts that the tail entry of // msgs[i].Buffers is the shared neverGSOEqualTailSentinelPayload slice. wantSentinelAtTail []bool }{ { name: "one-message-no-coalesce", buffs: [][]byte{ withGeneveSpace(1, 1), }, wantLens: [][]int{{1}}, wantGSO: []int{0}, }, { name: "one-message-no-coalesce-vni-isSet", buffs: [][]byte{ withGeneveSpace(1, 1), }, geneve: geneve, wantLens: [][]int{{1 + packet.GeneveFixedHeaderLength}}, wantGSO: []int{0}, }, { name: "two-messages-equal-len-coalesce", buffs: [][]byte{ withGeneveSpace(1, 2), withGeneveSpace(1, 1), }, wantLens: [][]int{{1, 1}}, wantGSO: []int{1}, }, { name: "two-messages-equal-len-coalesce-vni-isSet", buffs: [][]byte{ withGeneveSpace(1, 2+packet.GeneveFixedHeaderLength), withGeneveSpace(1, 1), }, geneve: geneve, wantLens: [][]int{{1 + packet.GeneveFixedHeaderLength, 1 + packet.GeneveFixedHeaderLength}}, wantGSO: []int{1 + packet.GeneveFixedHeaderLength}, }, { name: "two-messages-unequal-len-coalesce", buffs: [][]byte{ withGeneveSpace(2, 3), withGeneveSpace(1, 1), }, wantLens: [][]int{{2, 1}}, wantGSO: []int{2}, }, { name: "two-messages-unequal-len-coalesce-vni-isSet", buffs: [][]byte{ withGeneveSpace(2, 3+packet.GeneveFixedHeaderLength), withGeneveSpace(1, 1), }, geneve: geneve, wantLens: [][]int{{2 + packet.GeneveFixedHeaderLength, 1 + packet.GeneveFixedHeaderLength}}, wantGSO: []int{2 + packet.GeneveFixedHeaderLength}, }, { name: "three-messages-second-unequal-len-coalesce", buffs: [][]byte{ withGeneveSpace(2, 3), withGeneveSpace(1, 1), withGeneveSpace(2, 2), }, wantLens: [][]int{{2, 1}, {2}}, wantGSO: []int{2, 0}, }, { name: "three-messages-second-unequal-len-coalesce-vni-isSet", buffs: [][]byte{ withGeneveSpace(2, 3+(2*packet.GeneveFixedHeaderLength)), withGeneveSpace(1, 1), withGeneveSpace(2, 2), }, geneve: geneve, wantLens: [][]int{{2 + packet.GeneveFixedHeaderLength, 1 + packet.GeneveFixedHeaderLength}, {2 + packet.GeneveFixedHeaderLength}}, wantGSO: []int{2 + packet.GeneveFixedHeaderLength, 0}, }, { name: "three-messages-limited-cap-coalesce", buffs: [][]byte{ withGeneveSpace(2, 4), withGeneveSpace(2, 2), withGeneveSpace(2, 2), }, wantLens: [][]int{{2, 2, 2}}, wantGSO: []int{2}, }, { name: "three-messages-limited-cap-coalesce-vni-isSet", buffs: [][]byte{ withGeneveSpace(2, 4+packet.GeneveFixedHeaderLength), withGeneveSpace(2, 2), withGeneveSpace(2, 2), }, geneve: geneve, wantLens: [][]int{{2 + packet.GeneveFixedHeaderLength, 2 + packet.GeneveFixedHeaderLength, 2 + packet.GeneveFixedHeaderLength}}, wantGSO: []int{2 + packet.GeneveFixedHeaderLength}, }, { name: "two-equal-len-coalesce-neverGSOEqualTail-appends-sentinel", buffs: [][]byte{ withGeneveSpace(3, 3), withGeneveSpace(3, 3), }, neverGSOEqualTail: true, wantLens: [][]int{{3, 3, len(neverGSOEqualTailSentinelPayload)}}, wantGSO: []int{3}, wantSentinelAtTail: []bool{true}, }, { name: "two-equal-len-coalesce-neverGSOEqualTail-vni-isSet-appends-sentinel", buffs: [][]byte{ withGeneveSpace(3, 3+packet.GeneveFixedHeaderLength), withGeneveSpace(3, 3), }, geneve: geneve, neverGSOEqualTail: true, wantLens: [][]int{{3 + packet.GeneveFixedHeaderLength, 3 + packet.GeneveFixedHeaderLength, len(neverGSOEqualTailSentinelPayload)}}, wantGSO: []int{3 + packet.GeneveFixedHeaderLength}, wantSentinelAtTail: []bool{true}, }, { name: "two-unequal-len-coalesce-neverGSOEqualTail-smaller-tail-no-sentinel", buffs: [][]byte{ withGeneveSpace(3, 3), withGeneveSpace(2, 2), }, neverGSOEqualTail: true, wantLens: [][]int{{3, 2}}, wantGSO: []int{3}, }, { name: "one-byte-tail-neverGSOEqualTail-not-coalesced", // okToCoalesceWithSentinel is false when msgLen == 1 and // neverGSOEqualTail is set; the 1-byte tail is split into // its own non-coalesced singleton msg. buffs: [][]byte{ withGeneveSpace(2, 2), withGeneveSpace(1, 1), }, neverGSOEqualTail: true, wantLens: [][]int{{2}, {1}}, wantGSO: []int{0, 0}, }, { name: "one-byte-tail-neverGSOEqualTail-vni-isSet-coalesced", // With vniIsSet, msgLen always includes the Geneve header, so // okToCoalesceWithSentinel is true even for "1-byte payloads". // The naturally smaller tail short-circuits the sentinel. buffs: [][]byte{ withGeneveSpace(2, 2+packet.GeneveFixedHeaderLength), withGeneveSpace(1, 1), }, geneve: geneve, neverGSOEqualTail: true, wantLens: [][]int{{2 + packet.GeneveFixedHeaderLength, 1 + packet.GeneveFixedHeaderLength}}, wantGSO: []int{2 + packet.GeneveFixedHeaderLength}, }, { name: "batch-boundary-sentinel-appended-on-prior-batch-neverGSOEqualTail", // The 4th buff (length 5) is larger than gsoSize=3 so it // closes the first batch. The first batch has dgramCnt > 1 and // no smaller tail, so the sentinel is appended before starting // the new batch. buffs: [][]byte{ withGeneveSpace(3, 3), withGeneveSpace(3, 3), withGeneveSpace(3, 3), withGeneveSpace(5, 5), }, neverGSOEqualTail: true, wantLens: [][]int{{3, 3, 3, len(neverGSOEqualTailSentinelPayload)}, {5}}, wantGSO: []int{3, 0}, wantSentinelAtTail: []bool{true, false}, }, { name: "single-buff-neverGSOEqualTail-no-sentinel", // Only one datagram, no GSO happening, no sentinel. buffs: [][]byte{ withGeneveSpace(3, 3), }, neverGSOEqualTail: true, wantLens: [][]int{{3}}, wantGSO: []int{0}, }, { name: "equal-len-then-smaller-tail-then-equal-neverGSOEqualTail", // The smaller tail ends the first batch with no sentinel // (variation already provided), then a second singleton batch // is started for the trailing equal-length buff. buffs: [][]byte{ withGeneveSpace(3, 3), withGeneveSpace(3, 3), withGeneveSpace(2, 2), withGeneveSpace(3, 3), }, neverGSOEqualTail: true, wantLens: [][]int{{3, 3, 2}, {3}}, wantGSO: []int{3, 0}, }, }
for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { c := &linuxBatchingConn{} addr := &net.UDPAddr{ IP: net.ParseIP("127.0.0.1"), Port: 1, } msgs := make([]ipv6.Message, len(tt.buffs)) for i := range msgs { msgs[i].Buffers = make([][]byte, 1) msgs[i].OOB = make([]byte, controlMessageSize) } got := c.coalesceMessages(addr, tt.geneve, tt.buffs, msgs, packet.GeneveFixedHeaderLength, tt.neverGSOEqualTail) if got != len(tt.wantLens) { t.Fatalf("got len %d want: %d", got, len(tt.wantLens)) } for i := range got { if msgs[i].Addr != addr { t.Errorf("msgs[%d].Addr != passed addr", i) } if len(msgs[i].Buffers) != len(tt.wantLens[i]) { t.Fatalf("len(msgs[%d].Buffers) %d != %d", i, len(msgs[i].Buffers), len(tt.wantLens[i])) } for j := range tt.wantLens[i] { gotLen := len(msgs[i].Buffers[j]) if gotLen != tt.wantLens[i][j] { t.Errorf("len(msgs[%d].Buffers[%d]) %d != %d", i, j, gotLen, tt.wantLens[i][j]) } }
wantSentinel := i < len(tt.wantSentinelAtTail) && tt.wantSentinelAtTail[i] if wantSentinel { tail := msgs[i].Buffers[len(msgs[i].Buffers)-1] if len(tail) != len(neverGSOEqualTailSentinelPayload) || &tail[0] != &neverGSOEqualTailSentinelPayload[0] { t.Errorf("msgs[%d] tail buffer is not neverGSOEqualTailSentinelPayload", i) } }
// coalesceMessages calls setGSOSizeInControl, which uses a cmsg // type of UDP_SEGMENT, and getGSOSizeInControl scans for a cmsg // type of UDP_GRO. Therefore, we have to use the lower-level // getDataFromControl in order to specify the cmsg type of // interest for this test. data, err := getDataFromControl(msgs[i].OOB, unix.SOL_UDP, unix.UDP_SEGMENT, 2) if err != nil { t.Fatalf("msgs[%d] getDataFromControl err: %v", i, err) } var gotGSO int if len(data) >= 2 { gotGSO = int(binary.NativeEndian.Uint16(data)) } if gotGSO != tt.wantGSO[i] { t.Errorf("msgs[%d] gsoSize %d != %d", i, gotGSO, tt.wantGSO[i]) } } }) }}
// fakeBatchWriter is an xnetBatchReaderWriter that records the Buffers length// and destination address of each message handed to WriteBatch, and optionally// fails the first call with an error that triggers neterror.ShouldDisableUDPGSO.type fakeBatchWriter struct { gotBuffersLen [][]int // Buffers len of each msg, per WriteBatch call gotAddrs [][]netip.AddrPort // Address of each msg, per WriteBatch call failFirst bool}
func (f *fakeBatchWriter) ReadBatch([]ipv6.Message, int) (int, error) { return 0, nil }
func (f *fakeBatchWriter) WriteBatch(msgs []ipv6.Message, _ int) (int, error) { snap := make([]int, len(msgs)) addrs := make([]netip.AddrPort, len(msgs)) for i := range msgs { snap[i] = len(msgs[i].Buffers) addrs[i] = msgs[i].Addr.(*net.UDPAddr).AddrPort() } f.gotBuffersLen = append(f.gotBuffersLen, snap) f.gotAddrs = append(f.gotAddrs, addrs) if f.failFirst && len(f.gotBuffersLen) == 1 { return 0, &os.SyscallError{Syscall: "sendmmsg", Err: unix.EIO} } return len(msgs), nil}
// Test_linuxBatchingConn_WriteBatchTo_resetsBuffersOnGSORetry verifies that// when a coalesced (scatter-gather) write fails and triggers the GSO-disable// goto retry, the non-coalesce retry pass resets each message's Buffers back to// length 1 rather than leaving stale iovecs appended by coalesceMessages.func Test_linuxBatchingConn_WriteBatchTo_resetsBuffersOnGSORetry(t *testing.T) { uc, err := net.ListenUDP("udp4", nil) // only for pc.LocalAddr() in the error path if err != nil { t.Fatal(err) } defer uc.Close()
xpc := &fakeBatchWriter{failFirst: true} c := &linuxBatchingConn{ pc: uc, xpc: xpc, msgsPool: sync.Pool{New: func() any { ua := &net.UDPAddr{IP: make([]byte, 16)} msgs := make([]ipv6.Message, 8) for i := range msgs { msgs[i].Buffers = make([][]byte, 1) msgs[i].Addr = ua msgs[i].OOB = make([]byte, controlMessageSize) } return &msgsBatch{writeBatchToUDPAddr: ua, msgs: msgs} }}, } c.txOffload.Store(true) // force the coalesce path on the first pass
// Two equal-length buffs coalesce into a single msg whose Buffers grows // to len 2 (scatter-gather) on the first pass. buffs := [][]byte{make([]byte, 32), make([]byte, 32)}
err = c.WriteBatchTo(buffs, netip.MustParseAddrPort("127.0.0.1:1"), packet.GeneveHeader{}, 0)
// The retry path always returns ErrUDPGSODisabled wrapping the retry's // result (nil here). if _, ok := errors.AsType[neterror.ErrUDPGSODisabled](err); !ok { t.Fatalf("got %v, want ErrUDPGSODisabled", err) } if len(xpc.gotBuffersLen) != 2 { t.Fatalf("got %d WriteBatch calls, want 2", len(xpc.gotBuffersLen)) } // First (coalesced) call: one msg with 2 iovecs — confirms the precondition // that coalesceMessages grew Buffers past length 1. if got := xpc.gotBuffersLen[0]; len(got) != 1 || got[0] != 2 { t.Fatalf("first call buffers = %v, want [2]", got) } // Retry (non-coalesce) call: sends one msg per buff... if got := len(xpc.gotBuffersLen[1]); got != len(buffs) { t.Fatalf("retry call sent %d msgs, want %d", got, len(buffs)) } // ...and the fix must have reset every msg's Buffers back to len 1. for i, n := range xpc.gotBuffersLen[1] { if n != 1 { t.Errorf("retry msg[%d] Buffers len = %d, want 1", i, n) } }}
// Test_linuxBatchingConn_WriteBatchTo_offsetStableOnNonCoalesceRetry verifies// that the Geneve header offset adjustment in the non-coalesce path is derived// fresh from the original offset on each pass, rather than accumulating across a// goto retry. The non-coalesce branch runs on both passes when neverGSOEqualTail// is set and the batch is small enough to skip coalescing: the first pass fails// with an error that disables GSO, and the retry re-enters the same branch.// Since callers pass offset == GeneveFixedHeaderLength, a stale (accumulating)// offset would underflow to -GeneveFixedHeaderLength and panic on buffs[i][-8:].func Test_linuxBatchingConn_WriteBatchTo_offsetStableOnNonCoalesceRetry(t *testing.T) { uc, err := net.ListenUDP("udp4", nil) // only for pc.LocalAddr() in the error path if err != nil { t.Fatal(err) } defer uc.Close()
xpc := &fakeBatchWriter{failFirst: true} c := &linuxBatchingConn{ pc: uc, xpc: xpc, msgsPool: sync.Pool{New: func() any { ua := &net.UDPAddr{IP: make([]byte, 16)} msgs := make([]ipv6.Message, appendSentinelTailBatchSizeThreshold) for i := range msgs { msgs[i].Buffers = make([][]byte, 1) msgs[i].Addr = ua msgs[i].OOB = make([]byte, controlMessageSize) } return &msgsBatch{writeBatchToUDPAddr: ua, msgs: msgs} }}, } c.txOffload.Store(true) // neverGSOEqualTail set + a sub-threshold batch forces the non-coalesce // path while txOffload is still enabled, so the GSO-disable retry re-enters // the non-coalesce branch a second time. var neverGSOEqualTail atomic.Bool neverGSOEqualTail.Store(true) c.neverGSOEqualTail = &neverGSOEqualTail
// VNI set so the non-coalesce branch performs the offset -= GeneveFixedHeaderLength // adjustment; offset == GeneveFixedHeaderLength as the production caller requires. geneve := packet.GeneveHeader{Protocol: packet.GeneveProtocolWireGuard} geneve.VNI.Set(1) offset := packet.GeneveFixedHeaderLength
// Stay below appendSentinelTailBatchSizeThreshold so coalescing is skipped // and we take the non-coalesce branch on both passes. const nBuffs = appendSentinelTailBatchSizeThreshold - 1 buffs := make([][]byte, nBuffs) for i := range buffs { buffs[i] = make([]byte, 32) }
// Must not panic: each pass recomputes the offset from the original. err = c.WriteBatchTo(buffs, netip.MustParseAddrPort("127.0.0.1:1"), geneve, offset)
if _, ok := errors.AsType[neterror.ErrUDPGSODisabled](err); !ok { t.Fatalf("got %v, want ErrUDPGSODisabled", err) } if len(xpc.gotBuffersLen) != 2 { t.Fatalf("got %d WriteBatch calls, want 2 (initial + retry)", len(xpc.gotBuffersLen)) } // Both passes take the non-coalesce branch: one msg per buff, no coalescing. for call, got := range xpc.gotBuffersLen { if len(got) != len(buffs) { t.Errorf("call %d sent %d msgs, want %d", call, len(got), len(buffs)) } }}
// Test_linuxBatchingConn_WriteBatchTo_setsZone verifies that every message// handed to WriteBatch is addressed to the destination passed to WriteBatchTo,// including its IPv6 zone. The destination [*net.UDPAddr] comes from a pooled// [msgsBatch], so a zone set by one write must not carry over to a later write// to a destination without one.func Test_linuxBatchingConn_WriteBatchTo_setsZone(t *testing.T) { // Hand out the same batch on every Get, even if the pool drops it, so each // write reuses the [*net.UDPAddr] left behind by the previous one. ua := &net.UDPAddr{IP: make([]byte, 16)} msgs := make([]ipv6.Message, 2) for i := range msgs { msgs[i].Buffers = make([][]byte, 1) msgs[i].Addr = ua msgs[i].OOB = make([]byte, controlMessageSize) } batch := &msgsBatch{writeBatchToUDPAddr: ua, msgs: msgs}
xpc := &fakeBatchWriter{} c := &linuxBatchingConn{ xpc: xpc, msgsPool: sync.Pool{New: func() any { return batch }}, }
dsts := []netip.AddrPort{ netip.MustParseAddrPort("[fe80::1%eth0]:41641"), netip.MustParseAddrPort("[2001:db8::1]:41641"), // zone must be cleared netip.MustParseAddrPort("[fe80::1%3]:41641"), // numeric zone netip.MustParseAddrPort("192.0.2.1:41641"), // zone must be cleared } // Send two messages to check that each gets the destination address. buffs := [][]byte{make([]byte, 32), make([]byte, 32)} for _, dst := range dsts { if err := c.WriteBatchTo(buffs, dst, packet.GeneveHeader{}, 0); err != nil { t.Fatalf("WriteBatchTo(%v) = %v", dst, err) } got := xpc.gotAddrs[len(xpc.gotAddrs)-1] if len(got) != len(buffs) { t.Fatalf("WriteBatchTo(%v) sent %d msgs, want %d", dst, len(got), len(buffs)) } for i, addr := range got { if addr != dst { t.Errorf("WriteBatchTo(%v) msg[%d] addr = %v, want %v", dst, i, addr, dst) } } }}
func TestMinReadBatchMsgsLen(t *testing.T) { // So long as magicsock uses [Conn], and [wireguard-go/conn.Bind] API is // shaped for wireguard-go to control packet memory, these values should be // aligned. if MaximumWriteBatchSize != conn.IdealBatchSize { t.Fatalf("MaximumWriteBatchSize: %d != conn.IdealBatchSize(): %d", MaximumWriteBatchSize, conn.IdealBatchSize) }}
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}
func gsoControl(gso uint16) []byte { msg := makeControlMsg(unix.SOL_UDP, unix.UDP_GRO, 2) binary.NativeEndian.PutUint16(msg[unix.SizeofCmsghdr:], gso) return msg}
func rxqOverflowsControl(count uint32) []byte { msg := makeControlMsg(unix.SOL_SOCKET, unix.SO_RXQ_OVFL, 4) binary.NativeEndian.PutUint32(msg[unix.SizeofCmsghdr:], count) return msg}
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)}
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, }, } 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
// len(msgs) == 0 conn.handleRXQOverflowCounter([]ipv6.Message{}, nil) c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(0))
// rxErr non-nil conn.handleRXQOverflowCounter([]ipv6.Message{{}}, 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), }}, nil) c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(1))
// nonzero counter, no change conn.handleRXQOverflowCounter([]ipv6.Message{{ OOB: control, NN: len(control), }}, nil) c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(1))
// counter rollover control = rxqOverflowsControl(0) conn.handleRXQOverflowCounter([]ipv6.Message{{ OOB: control, NN: len(control), }}, nil) c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(1+math.MaxUint32))}