diff --git a/go.mod b/go.mod index a4327bc..9968fd0 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,6 @@ require ( github.com/vishvananda/netns v0.0.5 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.33.0 // indirect ) diff --git a/go.sum b/go.sum index d3e5a9b..818da4a 100644 --- a/go.sum +++ b/go.sum @@ -217,8 +217,8 @@ golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= diff --git a/overlay/tuntap/device.go b/overlay/tuntap/device.go deleted file mode 100644 index 3fb149c..0000000 --- a/overlay/tuntap/device.go +++ /dev/null @@ -1,232 +0,0 @@ -package tuntap - -import ( - "fmt" - "net" - "os" - "time" - "unsafe" - - "github.com/hetznercloud/virtio-go/virtio" - "golang.org/x/sys/unix" -) - -// Documentation: -// https://docs.kernel.org/networking/tuntap.html -// Also worth a read: -// https://blog.cloudflare.com/virtual-networking-101-understanding-tap/ - -// Device represents a TUN/TAP device. -type Device struct { - name string - ifindex uint32 - mac net.HardwareAddr - file *os.File -} - -// NewDevice creates a new TUN/TAP device, brings it up, and returns a [Device] -// instance providing access to it. -// -// There are multiple options that can be passed to this constructor to -// influence device creation: -// - [WithName] -// - [WithDeviceType] -// - [WithVirtioNetHdr] -// - [WithInterfaceFlags] -// -// Remember to call [Device.Close] after use to free up resources. -func NewDevice(options ...Option) (_ *Device, err error) { - opts := optionDefaults - opts.apply(options) - if err = opts.validate(); err != nil { - return nil, fmt.Errorf("invalid options: %w", err) - } - - // Get a file descriptor. The device will exist as long as we keep this - // file descriptor open. - fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0666) - if err != nil { - return nil, fmt.Errorf("access tuntap driver: %w", err) - } - - // Create an interface request. When the name is empty, the kernel will - // auto-select one. - ifreq, err := unix.NewIfreq(opts.name) - if err != nil { - _ = unix.Close(fd) - return nil, fmt.Errorf("new ifreq: %w", err) - } - - // Create the new device. - ifreq.SetUint16(opts.ifreqFlags()) - if err = unix.IoctlIfreq(fd, unix.TUNSETIFF, ifreq); err != nil { - _ = unix.Close(fd) - return nil, fmt.Errorf("create device: %w", err) - } - - dev := Device{ - // The TUNSETIFF ioctl writes the actual name that was chosen for the - // device back to the request, so use that. - name: ifreq.Name(), - } - - // Make the file descriptor of the device non-blocking. This enables us to - // cancel reads after a timeout when no packets are arriving. - // This, and the call to NewFile has to happen after creating the device: - // https://github.com/golang/go/issues/30426#issuecomment-470330742 - // NewFile will recognize that the file descriptor is non-blocking and will - // configure polling for it. - if err = unix.SetNonblock(fd, true); err != nil { - _ = unix.Close(fd) - return nil, fmt.Errorf("make file descriptor non-blocking: %w", err) - } - - // By wrapping the file descriptor as an os.File, we not only have a - // convenient way to read and write, but also register a finalizer that - // closes the file descriptor when it's being garbage collected. - dev.file = os.NewFile(uintptr(fd), dev.name) - - // Make sure the device is removed when one of the following initialization - // steps fails. - defer func() { - if err != nil { - _ = dev.Close() - } - }() - - if opts.virtioNetHdr { - // Tell the device which size we use for our virtio_net_hdr. - err = unix.IoctlSetPointerInt(fd, unix.TUNSETVNETHDRSZ, virtio.NetHdrSize) - if err != nil { - return nil, fmt.Errorf("set vnethdr size: %w", err) - } - } - - // Tell the device which offloads are supported. - err = unix.IoctlSetInt(fd, unix.TUNSETOFFLOAD, opts.offloads) - if err != nil { - return nil, fmt.Errorf("set offloads: %w", err) - } - - // For the following ioctls we need just any AF_INET socket, so create one. - inet, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) - if err != nil { - return nil, fmt.Errorf("open inet socket: %w", err) - } - defer func() { _ = unix.Close(inet) }() - - // Set the interface flags to bring it up. - ifreq.SetUint16(unix.IFF_UP | opts.interfaceFlags) - if err = unix.IoctlIfreq(inet, unix.SIOCSIFFLAGS, ifreq); err != nil { - return nil, fmt.Errorf("set interface flags: %w", err) - } - - // Get the interface index. - if err = unix.IoctlIfreq(inet, unix.SIOCGIFINDEX, ifreq); err != nil { - return nil, fmt.Errorf("get interface index: %w", err) - } - dev.ifindex = ifreq.Uint32() - - // Get the MAC address. - // This ioctl writes a sockaddr into the data ifru section of the interface - // request struct. The MAC address is in the beginning of the - // sockaddr.sa_data section. - if err = unix.IoctlIfreq(inet, unix.SIOCGIFHWADDR, ifreq); err != nil { - return nil, fmt.Errorf("get mac address: %w", err) - } - dev.mac = unsafe.Slice((*byte)(unsafe.Pointer(ifreq)), 32)[16+2 : 16+8] - - return &dev, nil -} - -// Close closes the file descriptor behind this device. This will cause the -// TUN/TAP device to be removed. -func (dev *Device) Close() error { - if err := dev.file.Close(); err != nil { - return fmt.Errorf("close file descriptor: %w", err) - } - dev.file = nil - return nil -} - -// Name returns the name of this device. -func (dev *Device) Name() string { - dev.ensureInitialized() - return dev.name -} - -// Ifindex returns the interface index of this device. -func (dev *Device) Ifindex() uint32 { - dev.ensureInitialized() - return dev.ifindex -} - -// MAC returns the hardware address of this device. -func (dev *Device) MAC() net.HardwareAddr { - dev.ensureInitialized() - return dev.mac -} - -// File returns the [os.File] that is used to communicate with this device. -// If you access it directly, please be careful to not interfere with this -// implementation. -func (dev *Device) File() *os.File { - dev.ensureInitialized() - return dev.file -} - -// WritePacket writes the given packet to the TUN/TAP device. -// When the [WithVirtioNetHdr] option was enabled, then the caller is -// responsible to prepend the packet with a [virtio.NetHdr]. -func (dev *Device) WritePacket(packet []byte) error { - dev.ensureInitialized() - - _, err := dev.file.Write(packet) - if err != nil { - return fmt.Errorf("write %d bytes: %w", len(packet), err) - } - - return nil -} - -// ReadPacket reads the next available packet from the TUN/TAP device into the -// given buffer. Make sure that the buffer is large enough, otherwise only a -// part of the packet may be read. The number of read bytes will be returned. -// -// When the [WithVirtioNetHdr] option was enabled, then the read packet will be -// prepended with a [virtio.NetHdr]. The caller is responsible to handle it -// accordingly. -// -// A timeout can be given to limit the time this operation blocks. If no packet -// arrives within the given timeout, the read is canceled and an error that -// wraps [os.ErrDeadlineExceeded] is returned. Pass a timeout of zero to make -// this operation block infinitely. -func (dev *Device) ReadPacket(buf []byte, timeout time.Duration) (int, error) { - dev.ensureInitialized() - - // Make sure the read times out. This only works for files that support - // polling (see above). - // When no timeout is desired, passing the zero time removes the deadline. - var deadline time.Time - if timeout > 0 { - deadline = time.Now().Add(timeout) - } - if err := dev.file.SetReadDeadline(deadline); err != nil { - return 0, fmt.Errorf("set deadline: %w", err) - } - - n, err := dev.file.Read(buf) - if err != nil { - return n, fmt.Errorf("read up to %d bytes: %w", len(buf), err) - } - - return n, nil -} - -// ensureInitialized is used as a guard to prevent methods to be called on an -// uninitialized instance. -func (dev *Device) ensureInitialized() { - if dev.file == nil { - panic("device is not initialized") - } -} diff --git a/overlay/tuntap/device_test.go b/overlay/tuntap/device_test.go deleted file mode 100644 index 3efa1f5..0000000 --- a/overlay/tuntap/device_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package tuntap_test - -import ( - "net" - "os" - "testing" - "time" - - "github.com/gopacket/gopacket/afpacket" - "github.com/hetznercloud/virtio-go/internal/testsupport" - "github.com/hetznercloud/virtio-go/tuntap" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/sys/unix" -) - -func TestNewDevice(t *testing.T) { - testsupport.VirtrunOnly(t) - - t.Run("with static name", func(t *testing.T) { - const name = "test42" - dev, err := tuntap.NewDevice( - tuntap.WithDeviceType(tuntap.DeviceTypeTAP), - tuntap.WithName(name), - ) - require.NoError(t, err) - t.Cleanup(func() { - assert.NoError(t, dev.Close()) - }) - - assert.Equal(t, name, dev.Name()) - - iface, err := net.InterfaceByIndex(int(dev.Ifindex())) - assert.NoError(t, err) - assert.Equal(t, name, iface.Name) - assert.Equal(t, dev.MAC(), iface.HardwareAddr) - }) - - t.Run("with auto selected name", func(t *testing.T) { - dev, err := tuntap.NewDevice( - tuntap.WithDeviceType(tuntap.DeviceTypeTAP), - ) - require.NoError(t, err) - t.Cleanup(func() { - assert.NoError(t, dev.Close()) - }) - - assert.Contains(t, dev.Name(), "tap") - - iface, err := net.InterfaceByIndex(int(dev.Ifindex())) - assert.NoError(t, err) - assert.Equal(t, dev.Name(), iface.Name) - assert.Equal(t, dev.MAC(), iface.HardwareAddr) - }) -} - -func TestDevice_WritePacket(t *testing.T) { - testsupport.VirtrunOnly(t) - - dev, tPacket := setupTestDevice(t) - - // Write a test packet to the TAP device. - _, pkt := testsupport.TestPacket(t, dev.MAC(), 64) - assert.NoError(t, dev.WritePacket(pkt)) - - // Check if the packet arrived in the RAW socket. - data, _, err := tPacket.ReadPacketData() - assert.NoError(t, err) - assert.Equal(t, pkt, data) -} - -func TestDevice_ReadPacket(t *testing.T) { - testsupport.VirtrunOnly(t) - - dev, tPacket := setupTestDevice(t) - - // Write a test packet to the RAW socket. - _, pkt := testsupport.TestPacket(t, dev.MAC(), 64) - assert.NoError(t, tPacket.WritePacketData(pkt)) - - // Check if the packet arrived at the TAP device. - receiveBuf := make([]byte, 1024) - n, err := dev.ReadPacket(receiveBuf, time.Second) - assert.NoError(t, err) - assert.Equal(t, len(pkt), n) - assert.Equal(t, pkt, receiveBuf[:n]) -} - -func TestDevice_ReadPacket_Timeout(t *testing.T) { - testsupport.VirtrunOnly(t) - - dev, _ := setupTestDevice(t) - - // Try to receive a packet on the TAP device when none was sent. - // This should time out. - receiveBuf := make([]byte, 1024) - _, err := dev.ReadPacket(receiveBuf, 500*time.Millisecond) - assert.ErrorIs(t, err, os.ErrDeadlineExceeded) -} - -func setupTestDevice(t *testing.T) (*tuntap.Device, *afpacket.TPacket) { - t.Helper() - testsupport.VirtrunOnly(t) - - // Make sure the Linux kernel does not send router solicitations that may - // interfere with these tests. - testsupport.SetSysctl(t, "net.ipv6.conf.all.disable_ipv6", "1") - - // Create a TAP device. - dev, err := tuntap.NewDevice( - tuntap.WithDeviceType(tuntap.DeviceTypeTAP), - // Helps to stop the Linux kernel from sending packets on this - // interface. - tuntap.WithInterfaceFlags(unix.IFF_NOARP), - ) - require.NoError(t, err) - t.Cleanup(func() { - assert.NoError(t, dev.Close()) - }) - - // Open a RAW socket to capture packets arriving at the TAP device or - // write packets to it. - tPacket, err := afpacket.NewTPacket( - afpacket.SocketRaw, - afpacket.TPacketVersion3, - afpacket.OptInterface(dev.Name()), - ) - require.NoError(t, err) - t.Cleanup(tPacket.Close) - - return dev, tPacket -} diff --git a/overlay/tuntap/doc.go b/overlay/tuntap/doc.go deleted file mode 100644 index f3ef180..0000000 --- a/overlay/tuntap/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package tuntap provides methods to create TUN/TAP devices and send and -// receive packets on them. -package tuntap diff --git a/overlay/tuntap/options.go b/overlay/tuntap/options.go deleted file mode 100644 index b32cf04..0000000 --- a/overlay/tuntap/options.go +++ /dev/null @@ -1,116 +0,0 @@ -package tuntap - -import ( - "errors" - - "golang.org/x/sys/unix" -) - -// DeviceType is the TUN/TAP device type. -type DeviceType int - -const ( - // DeviceTypeTUN can be used to create TUN devices that operate on layer 3. - // Packets that are transported over TUN devices do not have an Ethernet - // header. - DeviceTypeTUN DeviceType = unix.IFF_TUN - // DeviceTypeTAP can be used to create TAP devices that operate on layer 2. - // Packets that are transported over TAP devices do have an Ethernet header. - DeviceTypeTAP DeviceType = unix.IFF_TAP -) - -type optionValues struct { - name string - deviceType DeviceType - virtioNetHdr bool - offloads int - interfaceFlags uint16 -} - -func (o *optionValues) apply(options []Option) { - for _, option := range options { - option(o) - } -} - -func (o *optionValues) validate() error { - if len(o.name) >= unix.IFNAMSIZ { - return errors.New("name must not be longer that 15 characters") - } - if o.deviceType != DeviceTypeTUN && o.deviceType != DeviceTypeTAP { - return errors.New("device type is required and must be either TUN or TAP") - } - return nil -} - -func (o *optionValues) ifreqFlags() uint16 { - flags := uint16(o.deviceType) - - // Disable the packet information prefix. - flags |= unix.IFF_NO_PI - - // Ensure the ioctl fails when a device with the same name already exists. - flags |= unix.IFF_TUN_EXCL - - if o.virtioNetHdr { - // Also requires the TUNSETVNETHDRSZ ioctl at a later time. - flags |= unix.IFF_VNET_HDR - } - - return flags -} - -var optionDefaults = optionValues{ - // Let the kernel auto-select a name. - name: "", - // Required. - deviceType: -1, - // Don't enable it by default to avoid surprises. - virtioNetHdr: false, - // Optional. No offload support advertised by default. - offloads: 0, - // Optional. IFF_UP will always be set. - interfaceFlags: 0, -} - -// Option can be passed to [NewDevice] to influence device creation. -type Option func(*optionValues) - -// WithName returns an [Option] that sets the name of the to be created device. -// This is optional. When no name is specified, the kernel will auto-select a -// name using the scheme "tunX" or "tapX". -func WithName(name string) Option { - return func(o *optionValues) { o.name = name } -} - -// WithDeviceType returns an [Option] that sets the type of device that should -// be created. -// This is required. -func WithDeviceType(deviceType DeviceType) Option { - return func(o *optionValues) { o.deviceType = deviceType } -} - -// WithVirtioNetHdr returns an [Option] that sets whether packets that are -// transported over the device are prepended with a [virtio.NetHdr]. -// This is optional and disabled by default. -func WithVirtioNetHdr(enable bool) Option { - return func(o *optionValues) { o.virtioNetHdr = enable } -} - -// WithOffloads returns an [Option] that sets the supported offloads that the -// device should advertise. This tells the kernel which offloads the owner of -// the device can deal with ([unix.TUN_F_CSUM] for example). -// This is optional. By default, no offloads are supported. -// When configured, then [WithVirtioNetHdr] should also be enabled. -func WithOffloads(offloads int) Option { - return func(o *optionValues) { o.offloads = offloads } -} - -// WithInterfaceFlags returns an [Option] that sets the flags that should be -// used when taking the created interface up. -// This is optional. The [unix.IFF_UP] flag will always be set. -// The [unix.IFF_NOARP] flag may be useful in some scenarios to avoid packets -// from the Linux networking stack interfering with your application. -func WithInterfaceFlags(flags uint16) Option { - return func(o *optionValues) { o.interfaceFlags = flags } -} diff --git a/overlay/tuntap/options_internal_test.go b/overlay/tuntap/options_internal_test.go deleted file mode 100644 index 75ddf4b..0000000 --- a/overlay/tuntap/options_internal_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package tuntap - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "golang.org/x/sys/unix" -) - -func TestOptionValues_Apply(t *testing.T) { - opts := optionDefaults - opts.apply([]Option{ - WithName("name"), - WithDeviceType(DeviceTypeTAP), - WithVirtioNetHdr(true), - WithOffloads(unix.TUN_F_CSUM), - WithInterfaceFlags(unix.IFF_NOARP), - }) - - assert.Equal(t, optionValues{ - name: "name", - deviceType: DeviceTypeTAP, - virtioNetHdr: true, - offloads: unix.TUN_F_CSUM, - interfaceFlags: unix.IFF_NOARP, - }, opts) -} - -func TestOptionValues_Validate(t *testing.T) { - tests := []struct { - name string - values optionValues - assertErr assert.ErrorAssertionFunc - }{ - { - name: "name too long", - values: optionValues{ - name: "thisisaverylongname", - deviceType: DeviceTypeTAP, - }, - assertErr: assert.Error, - }, - { - name: "device type missing", - values: optionValues{}, - assertErr: assert.Error, - }, - { - name: "invalid device type", - values: optionValues{ - deviceType: 999, - }, - assertErr: assert.Error, - }, - { - name: "valid minimal", - values: optionValues{ - deviceType: DeviceTypeTAP, - }, - assertErr: assert.NoError, - }, - { - name: "valid full", - values: optionValues{ - name: "name", - deviceType: DeviceTypeTAP, - virtioNetHdr: true, - offloads: unix.TUN_F_CSUM, - interfaceFlags: unix.IFF_NOARP, - }, - assertErr: assert.NoError, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.assertErr(t, tt.values.validate()) - }) - } -} diff --git a/overlay/vhost/ioctl_test.go b/overlay/vhost/ioctl_test.go index 1a5676a..0732b96 100644 --- a/overlay/vhost/ioctl_test.go +++ b/overlay/vhost/ioctl_test.go @@ -4,7 +4,7 @@ import ( "testing" "unsafe" - "github.com/hetznercloud/virtio-go/vhost" + "github.com/slackhq/nebula/overlay/vhost" "github.com/stretchr/testify/assert" ) diff --git a/overlay/vhostnet/device_internal_test.go b/overlay/vhostnet/device_internal_test.go deleted file mode 100644 index 5f6e4f2..0000000 --- a/overlay/vhostnet/device_internal_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package vhostnet - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTruncateBuffers(t *testing.T) { - tests := []struct { - name string - buffers [][]byte - length int - expected [][]byte - }{ - { - name: "no buffers", - buffers: nil, - length: 0, - expected: nil, - }, - { - name: "single buffer correct length", - buffers: [][]byte{ - make([]byte, 100), - }, - length: 100, - expected: [][]byte{ - make([]byte, 100), - }, - }, - { - name: "single buffer truncated", - buffers: [][]byte{ - make([]byte, 100), - }, - length: 90, - expected: [][]byte{ - make([]byte, 90), - }, - }, - { - name: "multiple buffers correct length", - buffers: [][]byte{ - make([]byte, 200), - make([]byte, 100), - }, - length: 300, - expected: [][]byte{ - make([]byte, 200), - make([]byte, 100), - }, - }, - { - name: "multiple buffers truncated", - buffers: [][]byte{ - make([]byte, 200), - make([]byte, 100), - }, - length: 250, - expected: [][]byte{ - make([]byte, 200), - make([]byte, 50), - }, - }, - { - name: "multiple buffers truncated buffer list", - buffers: [][]byte{ - make([]byte, 200), - make([]byte, 200), - make([]byte, 200), - }, - length: 350, - expected: [][]byte{ - make([]byte, 200), - make([]byte, 150), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - actual := truncateBuffers(tt.buffers, tt.length) - assert.Equal(t, tt.expected, actual) - }) - } -} diff --git a/overlay/vhostnet/device_test.go b/overlay/vhostnet/device_test.go deleted file mode 100644 index d59b96c..0000000 --- a/overlay/vhostnet/device_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package vhostnet_test - -import ( - "fmt" - "os" - "sync" - "testing" - - "github.com/gopacket/gopacket/afpacket" - "github.com/hetznercloud/virtio-go/internal/testsupport" - "github.com/hetznercloud/virtio-go/tuntap" - "github.com/hetznercloud/virtio-go/vhostnet" - "github.com/hetznercloud/virtio-go/virtio" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/sys/unix" -) - -// Here is the general idea of how the following tests work to verify the -// correct communication with the vhost-net device within the kernel: -// -// +-----------------------------------+ -// | go test running in user space | -// +-----------------------------------+ -// ^ ^ -// | | -// capture / write transmit / receive -// using AF_PACKET using this package -// | | -// v v -// +----------------+ +-----------+ -// | tun (TAP mode) |<---->| vhost-net | -// +----------------+ +-----------+ -// - -func TestDevice_TransmitPacket(t *testing.T) { - testsupport.VirtrunOnly(t) - - fx := NewTestFixture(t) - - for _, length := range []int{64, 1514, 9014, 64100} { - t.Run(fmt.Sprintf("%d byte packet", length), func(t *testing.T) { - vnethdr, pkt := testsupport.TestPacket(t, fx.TAPDevice.MAC(), length) - - // Transmit the packet over the vhost-net device. - require.NoError(t, fx.NetDevice.TransmitPacket(vnethdr, pkt)) - - // Check if the packet arrived at the TAP device. The virtio-net - // header should have been stripped by the TAP device. - data, _, err := fx.TPacket.ReadPacketData() - assert.NoError(t, err) - assert.Equal(t, pkt, data) - }) - } -} - -func TestDevice_ReceivePacket(t *testing.T) { - testsupport.VirtrunOnly(t) - - fx := NewTestFixture(t) - - for _, length := range []int{64, 1514, 9014, 64100} { - t.Run(fmt.Sprintf("%d byte packet", length), func(t *testing.T) { - vnethdr, pkt := testsupport.TestPacket(t, fx.TAPDevice.MAC(), length) - prependedPkt := testsupport.PrependPacket(t, vnethdr, pkt) - - // Write the prepended packet to the TAP device. - require.NoError(t, fx.TPacket.WritePacketData(prependedPkt)) - - // Try to receive the packet on the vhost-net device. - vnethdr, data, err := fx.NetDevice.ReceivePacket() - assert.NoError(t, err) - assert.Equal(t, pkt, data) - - // Large packets should have been received as multiple buffers. - assert.Equal(t, (len(prependedPkt)/os.Getpagesize())+1, int(vnethdr.NumBuffers)) - }) - } -} - -func TestDevice_TransmitManyPackets(t *testing.T) { - testsupport.VirtrunOnly(t) - - fx := NewTestFixture(t) - - // Test with a packet which does not fit into a single memory page. - vnethdr, pkt := testsupport.TestPacket(t, fx.TAPDevice.MAC(), 9014) - - const count = 1024 - var received int - - var wg sync.WaitGroup - wg.Go(func() { - for range count { - err := fx.NetDevice.TransmitPacket(vnethdr, pkt) - if !assert.NoError(t, err) { - return - } - } - }) - wg.Go(func() { - for range count { - data, _, err := fx.TPacket.ReadPacketData() - if !assert.NoError(t, err) { - return - } - assert.Equal(t, pkt, data) - received++ - } - }) - wg.Wait() - - assert.Equal(t, count, received) -} - -func TestDevice_ReceiveManyPackets(t *testing.T) { - testsupport.VirtrunOnly(t) - - fx := NewTestFixture(t) - - // Test with a packet which does not fit into a single memory page. - vnethdr, pkt := testsupport.TestPacket(t, fx.TAPDevice.MAC(), 9014) - prependedPkt := testsupport.PrependPacket(t, vnethdr, pkt) - - const count = 1024 - var received int - - var wg sync.WaitGroup - wg.Go(func() { - for range count { - err := fx.TPacket.WritePacketData(prependedPkt) - if !assert.NoError(t, err) { - return - } - } - }) - wg.Go(func() { - for range count { - _, data, err := fx.NetDevice.ReceivePacket() - if !assert.NoError(t, err) { - return - } - assert.Equal(t, pkt, data) - received++ - } - }) - wg.Wait() - - assert.Equal(t, count, received) -} - -type TestFixture struct { - TAPDevice *tuntap.Device - NetDevice *vhostnet.Device - TPacket *afpacket.TPacket -} - -func NewTestFixture(t *testing.T) *TestFixture { - testsupport.VirtrunOnly(t) - - // In case something doesn't work, some more debug logging from the kernel - // modules may be very helpful. - testsupport.EnableDynamicDebug(t, "module tun") - testsupport.EnableDynamicDebug(t, "module vhost") - testsupport.EnableDynamicDebug(t, "module vhost_net") - - // Make sure the Linux kernel does not send router solicitations that may - // interfere with these tests. - testsupport.SetSysctl(t, "net.ipv6.conf.all.disable_ipv6", "1") - - var ( - fx TestFixture - err error - ) - - // Create a TAP device. - fx.TAPDevice, err = tuntap.NewDevice( - tuntap.WithDeviceType(tuntap.DeviceTypeTAP), - // Helps to stop the Linux kernel from sending packets on this - // interface. - tuntap.WithInterfaceFlags(unix.IFF_NOARP), - // Packets going over this device are prepended with a virtio-net - // header. When this is not set, then packets written to the TAP device - // will be passed to the Linux network stack without their virtio-net - // header stripped. - tuntap.WithVirtioNetHdr(true), - // When writing packets into the TAP device using the RAW socket, we - // don't want the offloads to be applied by the kernel. Advertising - // offload support makes the kernel pass the offload request along to - // our vhost-net device. - tuntap.WithOffloads(unix.TUN_F_CSUM|unix.TUN_F_USO4|unix.TUN_F_USO6), - ) - require.NoError(t, err) - t.Cleanup(func() { - assert.NoError(t, fx.TAPDevice.Close()) - }) - - // Create a vhost-net device that uses the TAP device as the backend. - fx.NetDevice, err = vhostnet.NewDevice( - vhostnet.WithQueueSize(32), - vhostnet.WithBackendDevice(fx.TAPDevice), - ) - require.NoError(t, err) - t.Cleanup(func() { - assert.NoError(t, fx.NetDevice.Close()) - }) - - // Open a RAW socket to capture packets arriving at the TAP device or - // write packets into it. - fx.TPacket, err = afpacket.NewTPacket( - afpacket.SocketRaw, - afpacket.TPacketVersion3, - afpacket.OptInterface(fx.TAPDevice.Name()), - - // Tell the kernel that packets written to this socket are prepended - // with a virto-net header. This is used to communicate the use of GSO - // for large packets. - afpacket.OptVNetHdrSize(virtio.NetHdrSize), - ) - require.NoError(t, err) - t.Cleanup(fx.TPacket.Close) - - return &fx -} diff --git a/overlay/vhostnet/options_internal_test.go b/overlay/vhostnet/options_internal_test.go deleted file mode 100644 index fe79ae8..0000000 --- a/overlay/vhostnet/options_internal_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package vhostnet - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestOptionValues_Apply(t *testing.T) { - opts := optionDefaults - opts.apply([]Option{ - WithQueueSize(256), - WithBackendFD(99), - }) - - assert.Equal(t, optionValues{ - queueSize: 256, - backendFD: 99, - }, opts) -} - -func TestOptionValues_Validate(t *testing.T) { - tests := []struct { - name string - values optionValues - assertErr assert.ErrorAssertionFunc - }{ - { - name: "queue size missing", - values: optionValues{ - queueSize: -1, - backendFD: 99, - }, - assertErr: assert.Error, - }, - { - name: "invalid queue size", - values: optionValues{ - queueSize: 24, - backendFD: 99, - }, - assertErr: assert.Error, - }, - { - name: "backend fd missing", - values: optionValues{ - queueSize: 256, - backendFD: -1, - }, - assertErr: assert.Error, - }, - { - name: "valid", - values: optionValues{ - queueSize: 256, - backendFD: 99, - }, - assertErr: assert.NoError, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.assertErr(t, tt.values.validate()) - }) - } -}