gvisor/pkg/tcpip/link/fdbased/endpoint_test.go

625 lines
16 KiB
Go
Raw Normal View History

// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build linux
package fdbased
import (
"bytes"
"fmt"
"math/rand"
"reflect"
"syscall"
"testing"
"time"
"unsafe"
"github.com/google/go-cmp/cmp"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
const (
mtu = 1500
laddr = tcpip.LinkAddress("\x11\x22\x33\x44\x55\x66")
raddr = tcpip.LinkAddress("\x77\x88\x99\xaa\xbb\xcc")
proto = 10
csumOffset = 48
gsoMSS = 500
)
type packetInfo struct {
Raddr tcpip.LinkAddress
Proto tcpip.NetworkProtocolNumber
Contents *stack.PacketBuffer
}
type packetContents struct {
LinkHeader buffer.View
NetworkHeader buffer.View
TransportHeader buffer.View
Data buffer.View
}
func checkPacketInfoEqual(t *testing.T, got, want packetInfo) {
t.Helper()
if diff := cmp.Diff(
want, got,
cmp.Transformer("ExtractPacketBuffer", func(pk *stack.PacketBuffer) *packetContents {
if pk == nil {
return nil
}
return &packetContents{
LinkHeader: pk.LinkHeader().View(),
NetworkHeader: pk.NetworkHeader().View(),
TransportHeader: pk.TransportHeader().View(),
Data: pk.Data.ToView(),
}
}),
); diff != "" {
t.Errorf("unexpected packetInfo (-want +got):\n%s", diff)
}
}
type context struct {
t *testing.T
readFDs []int
writeFDs []int
ep stack.LinkEndpoint
ch chan packetInfo
done chan struct{}
}
func newContext(t *testing.T, opt *Options) *context {
firstFDPair, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_SEQPACKET, 0)
if err != nil {
t.Fatalf("Socketpair failed: %v", err)
}
secondFDPair, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_SEQPACKET, 0)
if err != nil {
t.Fatalf("Socketpair failed: %v", err)
}
done := make(chan struct{}, 2)
opt.ClosedFunc = func(*tcpip.Error) {
done <- struct{}{}
}
opt.FDs = []int{firstFDPair[1], secondFDPair[1]}
ep, err := New(opt)
if err != nil {
t.Fatalf("Failed to create FD endpoint: %v", err)
}
c := &context{
t: t,
readFDs: []int{firstFDPair[0], secondFDPair[0]},
writeFDs: opt.FDs,
ep: ep,
ch: make(chan packetInfo, 100),
done: done,
}
ep.Attach(c)
return c
}
func (c *context) cleanup() {
for _, fd := range c.readFDs {
syscall.Close(fd)
}
<-c.done
<-c.done
for _, fd := range c.writeFDs {
syscall.Close(fd)
}
}
func (c *context) DeliverNetworkPacket(remote tcpip.LinkAddress, local tcpip.LinkAddress, protocol tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) {
Use PacketBuffers, rather than VectorisedViews, in netstack. PacketBuffers are analogous to Linux's sk_buff. They hold all information about a packet, headers, and payload. This is important for: * iptables to access various headers of packets * Preventing the clutter of passing different net and link headers along with VectorisedViews to packet handling functions. This change only affects the incoming packet path, and a future change will change the outgoing path. Benchmark Regular PacketBufferPtr PacketBufferConcrete -------------------------------------------------------------------------------- BM_Recvmsg 400.715MB/s 373.676MB/s 396.276MB/s BM_Sendmsg 361.832MB/s 333.003MB/s 335.571MB/s BM_Recvfrom 453.336MB/s 393.321MB/s 381.650MB/s BM_Sendto 378.052MB/s 372.134MB/s 341.342MB/s BM_SendmsgTCP/0/1k 353.711MB/s 316.216MB/s 322.747MB/s BM_SendmsgTCP/0/2k 600.681MB/s 588.776MB/s 565.050MB/s BM_SendmsgTCP/0/4k 995.301MB/s 888.808MB/s 941.888MB/s BM_SendmsgTCP/0/8k 1.517GB/s 1.274GB/s 1.345GB/s BM_SendmsgTCP/0/16k 1.872GB/s 1.586GB/s 1.698GB/s BM_SendmsgTCP/0/32k 1.017GB/s 1.020GB/s 1.133GB/s BM_SendmsgTCP/0/64k 475.626MB/s 584.587MB/s 627.027MB/s BM_SendmsgTCP/0/128k 416.371MB/s 503.434MB/s 409.850MB/s BM_SendmsgTCP/0/256k 323.449MB/s 449.599MB/s 388.852MB/s BM_SendmsgTCP/0/512k 243.992MB/s 267.676MB/s 314.474MB/s BM_SendmsgTCP/0/1M 95.138MB/s 95.874MB/s 95.417MB/s BM_SendmsgTCP/0/2M 96.261MB/s 94.977MB/s 96.005MB/s BM_SendmsgTCP/0/4M 96.512MB/s 95.978MB/s 95.370MB/s BM_SendmsgTCP/0/8M 95.603MB/s 95.541MB/s 94.935MB/s BM_SendmsgTCP/0/16M 94.598MB/s 94.696MB/s 94.521MB/s BM_SendmsgTCP/0/32M 94.006MB/s 94.671MB/s 94.768MB/s BM_SendmsgTCP/0/64M 94.133MB/s 94.333MB/s 94.746MB/s BM_SendmsgTCP/0/128M 93.615MB/s 93.497MB/s 93.573MB/s BM_SendmsgTCP/0/256M 93.241MB/s 95.100MB/s 93.272MB/s BM_SendmsgTCP/1/1k 303.644MB/s 316.074MB/s 308.430MB/s BM_SendmsgTCP/1/2k 537.093MB/s 584.962MB/s 529.020MB/s BM_SendmsgTCP/1/4k 882.362MB/s 939.087MB/s 892.285MB/s BM_SendmsgTCP/1/8k 1.272GB/s 1.394GB/s 1.296GB/s BM_SendmsgTCP/1/16k 1.802GB/s 2.019GB/s 1.830GB/s BM_SendmsgTCP/1/32k 2.084GB/s 2.173GB/s 2.156GB/s BM_SendmsgTCP/1/64k 2.515GB/s 2.463GB/s 2.473GB/s BM_SendmsgTCP/1/128k 2.811GB/s 3.004GB/s 2.946GB/s BM_SendmsgTCP/1/256k 3.008GB/s 3.159GB/s 3.171GB/s BM_SendmsgTCP/1/512k 2.980GB/s 3.150GB/s 3.126GB/s BM_SendmsgTCP/1/1M 2.165GB/s 2.233GB/s 2.163GB/s BM_SendmsgTCP/1/2M 2.370GB/s 2.219GB/s 2.453GB/s BM_SendmsgTCP/1/4M 2.005GB/s 2.091GB/s 2.214GB/s BM_SendmsgTCP/1/8M 2.111GB/s 2.013GB/s 2.109GB/s BM_SendmsgTCP/1/16M 1.902GB/s 1.868GB/s 1.897GB/s BM_SendmsgTCP/1/32M 1.655GB/s 1.665GB/s 1.635GB/s BM_SendmsgTCP/1/64M 1.575GB/s 1.547GB/s 1.575GB/s BM_SendmsgTCP/1/128M 1.524GB/s 1.584GB/s 1.580GB/s BM_SendmsgTCP/1/256M 1.579GB/s 1.607GB/s 1.593GB/s PiperOrigin-RevId: 278940079
2019-11-06 22:24:38 +00:00
c.ch <- packetInfo{remote, protocol, pkt}
}
func (c *context) DeliverOutboundPacket(remote tcpip.LinkAddress, local tcpip.LinkAddress, protocol tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) {
panic("unimplemented")
}
func TestNoEthernetProperties(t *testing.T) {
c := newContext(t, &Options{MTU: mtu})
defer c.cleanup()
if want, v := uint16(0), c.ep.MaxHeaderLength(); want != v {
t.Fatalf("MaxHeaderLength() = %v, want %v", v, want)
}
if want, v := uint32(mtu), c.ep.MTU(); want != v {
t.Fatalf("MTU() = %v, want %v", v, want)
}
}
func TestEthernetProperties(t *testing.T) {
c := newContext(t, &Options{EthernetHeader: true, MTU: mtu})
defer c.cleanup()
if want, v := uint16(header.EthernetMinimumSize), c.ep.MaxHeaderLength(); want != v {
t.Fatalf("MaxHeaderLength() = %v, want %v", v, want)
}
if want, v := uint32(mtu), c.ep.MTU(); want != v {
t.Fatalf("MTU() = %v, want %v", v, want)
}
}
func TestAddress(t *testing.T) {
addrs := []tcpip.LinkAddress{"", "abc", "def"}
for _, a := range addrs {
t.Run(fmt.Sprintf("Address: %q", a), func(t *testing.T) {
c := newContext(t, &Options{Address: a, MTU: mtu})
defer c.cleanup()
if want, v := a, c.ep.LinkAddress(); want != v {
t.Fatalf("LinkAddress() = %v, want %v", v, want)
}
})
}
}
func testWritePacket(t *testing.T, plen int, eth bool, gsoMaxSize uint32, hash uint32) {
c := newContext(t, &Options{Address: laddr, MTU: mtu, EthernetHeader: eth, GSOMaxSize: gsoMaxSize})
defer c.cleanup()
var r stack.RouteInfo
r.RemoteLinkAddress = raddr
// Build payload.
payload := buffer.NewView(plen)
if _, err := rand.Read(payload); err != nil {
t.Fatalf("rand.Read(payload): %s", err)
}
// Build packet buffer.
const netHdrLen = 100
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: int(c.ep.MaxHeaderLength()) + netHdrLen,
Data: payload.ToVectorisedView(),
})
pkt.Hash = hash
// Build header.
b := pkt.NetworkHeader().Push(netHdrLen)
if _, err := rand.Read(b); err != nil {
t.Fatalf("rand.Read(b): %s", err)
}
// Write.
want := append(append(buffer.View(nil), b...), payload...)
var gso *stack.GSO
if gsoMaxSize != 0 {
gso = &stack.GSO{
Type: stack.GSOTCPv6,
NeedsCsum: true,
CsumOffset: csumOffset,
MSS: gsoMSS,
MaxSize: gsoMaxSize,
L3HdrLen: header.IPv4MaximumHeaderSize,
}
}
if err := c.ep.WritePacket(r, gso, proto, pkt); err != nil {
t.Fatalf("WritePacket failed: %v", err)
}
// Read from the corresponding FD, then compare with what we wrote.
b = make([]byte, mtu)
fd := c.readFDs[hash%uint32(len(c.readFDs))]
n, err := syscall.Read(fd, b)
if err != nil {
t.Fatalf("Read failed: %v", err)
}
b = b[:n]
if gsoMaxSize != 0 {
vnetHdr := *(*virtioNetHdr)(unsafe.Pointer(&b[0]))
if vnetHdr.flags&_VIRTIO_NET_HDR_F_NEEDS_CSUM == 0 {
t.Fatalf("virtioNetHdr.flags %v doesn't contain %v", vnetHdr.flags, _VIRTIO_NET_HDR_F_NEEDS_CSUM)
}
csumStart := header.EthernetMinimumSize + gso.L3HdrLen
if vnetHdr.csumStart != csumStart {
t.Fatalf("vnetHdr.csumStart = %v, want %v", vnetHdr.csumStart, csumStart)
}
if vnetHdr.csumOffset != csumOffset {
t.Fatalf("vnetHdr.csumOffset = %v, want %v", vnetHdr.csumOffset, csumOffset)
}
gsoType := uint8(0)
if int(gso.MSS) < plen {
gsoType = _VIRTIO_NET_HDR_GSO_TCPV6
}
if vnetHdr.gsoType != gsoType {
t.Fatalf("vnetHdr.gsoType = %v, want %v", vnetHdr.gsoType, gsoType)
}
b = b[virtioNetHdrSize:]
}
if eth {
h := header.Ethernet(b)
b = b[header.EthernetMinimumSize:]
if a := h.SourceAddress(); a != laddr {
t.Fatalf("SourceAddress() = %v, want %v", a, laddr)
}
if a := h.DestinationAddress(); a != raddr {
t.Fatalf("DestinationAddress() = %v, want %v", a, raddr)
}
if et := h.Type(); et != proto {
t.Fatalf("Type() = %v, want %v", et, proto)
}
}
if len(b) != len(want) {
t.Fatalf("Read returned %v bytes, want %v", len(b), len(want))
}
if !bytes.Equal(b, want) {
t.Fatalf("Read returned %x, want %x", b, want)
}
}
func TestWritePacket(t *testing.T) {
lengths := []int{0, 100, 1000}
eths := []bool{true, false}
gsos := []uint32{0, 32768}
for _, eth := range eths {
for _, plen := range lengths {
for _, gso := range gsos {
t.Run(
fmt.Sprintf("Eth=%v,PayloadLen=%v,GSOMaxSize=%v", eth, plen, gso),
func(t *testing.T) {
testWritePacket(t, plen, eth, gso, 0)
},
)
}
}
}
}
func TestHashedWritePacket(t *testing.T) {
lengths := []int{0, 100, 1000}
eths := []bool{true, false}
gsos := []uint32{0, 32768}
hashes := []uint32{0, 1}
for _, eth := range eths {
for _, plen := range lengths {
for _, gso := range gsos {
for _, hash := range hashes {
t.Run(
fmt.Sprintf("Eth=%v,PayloadLen=%v,GSOMaxSize=%v,Hash=%d", eth, plen, gso, hash),
func(t *testing.T) {
testWritePacket(t, plen, eth, gso, hash)
},
)
}
}
}
}
}
func TestPreserveSrcAddress(t *testing.T) {
baddr := tcpip.LinkAddress("\xcc\xbb\xaa\x77\x88\x99")
c := newContext(t, &Options{Address: laddr, MTU: mtu, EthernetHeader: true})
defer c.cleanup()
// Set LocalLinkAddress in route to the value of the bridged address.
var r stack.RouteInfo
Invoke address resolution upon subsequent traffic to Failed neighbor Removes the period of time in which subseqeuent traffic to a Failed neighbor immediately fails with ErrNoLinkAddress. A Failed neighbor is one in which address resolution fails; or in other words, the neighbor's IP address cannot be translated to a MAC address. This means removing the Failed state for linkAddrCache and allowing transitiong out of Failed into Incomplete for neighborCache. Previously, both caches would transition entries to Failed after address resolution fails. In this state, any subsequent traffic requested within an unreachable time would immediately fail with ErrNoLinkAddress. This does not follow RFC 4861 section 7.3.3: If address resolution fails, the entry SHOULD be deleted, so that subsequent traffic to that neighbor invokes the next-hop determination procedure again. Invoking next-hop determination at this point ensures that alternate default routers are tried. The API for getting a link address for a given address, whether through the link address cache or the neighbor table, is updated to optionally take a callback which will be called when address resolution completes. This allows `Route` to handle completing link resolution internally, so callers of (*Route).Resolve (e.g. endpoints) don’t have to keep track of when it completes and update the Route accordingly. This change also removes the wakers from LinkAddressCache, NeighborCache, and Route in favor of the callbacks, and callers that previously used a waker can now just pass a callback to (*Route).Resolve that will notify the waker on resolution completion. Fixes #4796 Startblock: has LGTM from sbalana and then add reviewer ghanan PiperOrigin-RevId: 348597478
2020-12-22 09:34:41 +00:00
r.LocalLinkAddress = baddr
r.RemoteLinkAddress = raddr
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
// WritePacket panics given a prependable with anything less than
// the minimum size of the ethernet header.
// TODO(b/153685824): Figure out if this should use c.ep.MaxHeaderLength().
ReserveHeaderBytes: header.EthernetMinimumSize,
Data: buffer.VectorisedView{},
})
if err := c.ep.WritePacket(r, nil /* gso */, proto, pkt); err != nil {
t.Fatalf("WritePacket failed: %v", err)
}
// Read from the FD, then compare with what we wrote.
b := make([]byte, mtu)
n, err := syscall.Read(c.readFDs[0], b)
if err != nil {
t.Fatalf("Read failed: %v", err)
}
b = b[:n]
h := header.Ethernet(b)
if a := h.SourceAddress(); a != baddr {
t.Fatalf("SourceAddress() = %v, want %v", a, baddr)
}
}
func TestDeliverPacket(t *testing.T) {
lengths := []int{100, 1000}
eths := []bool{true, false}
for _, eth := range eths {
for _, plen := range lengths {
t.Run(fmt.Sprintf("Eth=%v,PayloadLen=%v", eth, plen), func(t *testing.T) {
c := newContext(t, &Options{Address: laddr, MTU: mtu, EthernetHeader: eth})
defer c.cleanup()
// Build packet.
all := make([]byte, plen)
if _, err := rand.Read(all); err != nil {
t.Fatalf("rand.Read(all): %s", err)
}
// Make it look like an IPv4 packet.
all[0] = 0x40
wantPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: header.EthernetMinimumSize,
Data: buffer.NewViewFromBytes(all).ToVectorisedView(),
})
if eth {
hdr := header.Ethernet(wantPkt.LinkHeader().Push(header.EthernetMinimumSize))
hdr.Encode(&header.EthernetFields{
SrcAddr: raddr,
DstAddr: laddr,
Type: proto,
})
all = append(hdr, all...)
}
// Write packet via the file descriptor.
if _, err := syscall.Write(c.readFDs[0], all); err != nil {
t.Fatalf("Write failed: %v", err)
}
// Receive packet through the endpoint.
select {
case pi := <-c.ch:
want := packetInfo{
Raddr: raddr,
Proto: proto,
Contents: wantPkt,
}
if !eth {
want.Proto = header.IPv4ProtocolNumber
want.Raddr = ""
}
checkPacketInfoEqual(t, pi, want)
case <-time.After(10 * time.Second):
t.Fatalf("Timed out waiting for packet")
}
})
}
}
}
func TestBufConfigMaxLength(t *testing.T) {
got := 0
for _, i := range BufConfig {
got += i
}
want := header.MaxIPPacketSize // maximum TCP packet size
if got < want {
t.Errorf("total buffer size is invalid: got %d, want >= %d", got, want)
}
}
func TestBufConfigFirst(t *testing.T) {
// The stack assumes that the TCP/IP header is enterily contained in the first view.
// Therefore, the first view needs to be large enough to contain the maximum TCP/IP
// header, which is 120 bytes (60 bytes for IP + 60 bytes for TCP).
want := 120
got := BufConfig[0]
if got < want {
t.Errorf("first view has an invalid size: got %d, want >= %d", got, want)
}
}
var capLengthTestCases = []struct {
comment string
config []int
n int
wantUsed int
wantLengths []int
}{
{
comment: "Single slice",
config: []int{2},
n: 1,
wantUsed: 1,
wantLengths: []int{1},
},
{
comment: "Multiple slices",
config: []int{1, 2},
n: 2,
wantUsed: 2,
wantLengths: []int{1, 1},
},
{
comment: "Entire buffer",
config: []int{1, 2},
n: 3,
wantUsed: 2,
wantLengths: []int{1, 2},
},
{
comment: "Entire buffer but not on the last slice",
config: []int{1, 2, 3},
n: 3,
wantUsed: 2,
wantLengths: []int{1, 2},
},
}
func TestIovecBuffer(t *testing.T) {
for _, c := range capLengthTestCases {
t.Run(c.comment, func(t *testing.T) {
b := newIovecBuffer(c.config, false /* skipsVnetHdr */)
// Test initial allocation.
iovecs := b.nextIovecs()
if got, want := len(iovecs), len(c.config); got != want {
t.Fatalf("len(iovecs) = %d, want %d", got, want)
}
// Make a copy as iovecs points to internal slice. We will need this state
// later.
oldIovecs := append([]syscall.Iovec(nil), iovecs...)
// Test the views that get pulled.
vv := b.pullViews(c.n)
var lengths []int
for _, v := range vv.Views() {
lengths = append(lengths, len(v))
}
if !reflect.DeepEqual(lengths, c.wantLengths) {
t.Errorf("Pulled view lengths = %v, want %v", lengths, c.wantLengths)
}
// Test that new views get reallocated.
for i, newIov := range b.nextIovecs() {
if i < c.wantUsed {
if newIov.Base == oldIovecs[i].Base {
t.Errorf("b.views[%d] should have been reallocated", i)
}
} else {
if newIov.Base != oldIovecs[i].Base {
t.Errorf("b.views[%d] should not have been reallocated", i)
}
}
}
})
}
}
func TestIovecBufferSkipVnetHdr(t *testing.T) {
for _, test := range []struct {
desc string
readN int
wantLen int
}{
{
desc: "nothing read",
readN: 0,
wantLen: 0,
},
{
desc: "smaller than vnet header",
readN: virtioNetHdrSize - 1,
wantLen: 0,
},
{
desc: "header skipped",
readN: virtioNetHdrSize + 100,
wantLen: 100,
},
} {
t.Run(test.desc, func(t *testing.T) {
b := newIovecBuffer([]int{10, 20, 50, 50}, true)
// Pretend a read happend.
b.nextIovecs()
vv := b.pullViews(test.readN)
if got, want := vv.Size(), test.wantLen; got != want {
t.Errorf("b.pullView(%d).Size() = %d; want %d", test.readN, got, want)
}
if got, want := len(vv.ToOwnedView()), test.wantLen; got != want {
t.Errorf("b.pullView(%d).ToOwnedView() has length %d; want %d", test.readN, got, want)
}
})
}
}
// fakeNetworkDispatcher delivers packets to pkts.
type fakeNetworkDispatcher struct {
pkts []*stack.PacketBuffer
}
func (d *fakeNetworkDispatcher) DeliverNetworkPacket(remote, local tcpip.LinkAddress, protocol tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) {
d.pkts = append(d.pkts, pkt)
}
func (d *fakeNetworkDispatcher) DeliverOutboundPacket(remote, local tcpip.LinkAddress, protocol tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) {
panic("unimplemented")
}
func TestDispatchPacketFormat(t *testing.T) {
for _, test := range []struct {
name string
newDispatcher func(fd int, e *endpoint) (linkDispatcher, error)
}{
{
name: "readVDispatcher",
newDispatcher: newReadVDispatcher,
},
{
name: "recvMMsgDispatcher",
newDispatcher: newRecvMMsgDispatcher,
},
} {
t.Run(test.name, func(t *testing.T) {
// Create a socket pair to send/recv.
fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_DGRAM, 0)
if err != nil {
t.Fatal(err)
}
defer syscall.Close(fds[0])
defer syscall.Close(fds[1])
data := []byte{
// Ethernet header.
1, 2, 3, 4, 5, 60,
1, 2, 3, 4, 5, 61,
8, 0,
// Mock network header.
40, 41, 42, 43,
}
err = syscall.Sendmsg(fds[1], data, nil, nil, 0)
if err != nil {
t.Fatal(err)
}
// Create and run dispatcher once.
sink := &fakeNetworkDispatcher{}
d, err := test.newDispatcher(fds[0], &endpoint{
hdrSize: header.EthernetMinimumSize,
dispatcher: sink,
})
if err != nil {
t.Fatal(err)
}
if ok, err := d.dispatch(); !ok || err != nil {
t.Fatalf("d.dispatch() = %v, %v", ok, err)
}
// Verify packet.
if got, want := len(sink.pkts), 1; got != want {
t.Fatalf("len(sink.pkts) = %d, want %d", got, want)
}
pkt := sink.pkts[0]
if got, want := pkt.LinkHeader().View().Size(), header.EthernetMinimumSize; got != want {
t.Errorf("pkt.LinkHeader().View().Size() = %d, want %d", got, want)
}
if got, want := pkt.Data.Size(), 4; got != want {
t.Errorf("pkt.Data.Size() = %d, want %d", got, want)
}
})
}
}