Files
2024-12-03 15:21:54 -08:00

376 lines
11 KiB
Go

// Copyright 2022 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.
// Package context provides a context used by datagram-based network endpoints
// tests. It also defines the TestFlow type to facilitate IP configurations.
package context
import (
"bytes"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/time/rate"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/refs"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/checker"
"gvisor.dev/gvisor/pkg/tcpip/faketime"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
"gvisor.dev/gvisor/pkg/tcpip/link/sniffer"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/raw"
"gvisor.dev/gvisor/pkg/waiter"
)
const (
// NICID is the id of the nic created by the Context.
NICID = 1
// DefaultMTU is the MTU used by the Context, except where another value is
// explicitly specified during initialization. It is chosen to match the MTU
// of loopback interfaces on linux systems.
DefaultMTU = 65536
)
// Context is a testing context for datagram-based network endpoints.
type Context struct {
// T is the testing context.
T *testing.T
// LinkEP is the link endpoint that is attached to the stack's NIC.
LinkEP *channel.Endpoint
// Stack is the networking stack owned by the context.
Stack *stack.Stack
// EP is the transport endpoint owned by the context.
EP tcpip.Endpoint
// WQ is the wait queue associated with EP and is used to block for events on
// EP.
WQ waiter.Queue
}
// Options contains options for creating a new test context.
type Options struct {
// MTU is the mtu that the link endpoint will be initialized with.
MTU uint32
// HandleLocal specifies if non-loopback interfaces are allowed to loop
// packets.
HandleLocal bool
// EnableExperimentIPOption indicates whether the NIC is responsible for
// passing the experiment IP option.
EnableExperimentIPOption bool
}
// New allocates and initializes a test context containing a configured stack.
func New(t *testing.T, transportProtocols []stack.TransportProtocolFactory) *Context {
t.Helper()
options := Options{
MTU: DefaultMTU,
HandleLocal: true,
}
return NewWithOptions(t, transportProtocols, options)
}
// NewWithOptions allocates and initializes a test context containing a
// configured stack with the provided options.
func NewWithOptions(t *testing.T, transportProtocols []stack.TransportProtocolFactory, options Options) *Context {
t.Helper()
stackOptions := stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
TransportProtocols: transportProtocols,
HandleLocal: options.HandleLocal,
Clock: &faketime.NullClock{},
RawFactory: &raw.EndpointFactory{},
}
s := stack.New(stackOptions)
// Disable ICMP rate limiter since we're using Null clock, which never
// advances time and thus never allows ICMP messages.
s.SetICMPLimit(rate.Inf)
ep := channel.New(256, options.MTU, "")
wep := stack.LinkEndpoint(ep)
if testing.Verbose() {
wep = sniffer.New(ep)
}
if err := s.CreateNICWithOptions(NICID, wep, stack.NICOptions{Name: "nic1", EnableExperimentIPOption: options.EnableExperimentIPOption}); err != nil {
t.Fatalf("CreateNIC(%d, _): %s", NICID, err)
}
protocolAddrV4 := tcpip.ProtocolAddress{
Protocol: ipv4.ProtocolNumber,
AddressWithPrefix: tcpip.Address(StackAddr).WithPrefix(),
}
if err := s.AddProtocolAddress(NICID, protocolAddrV4, stack.AddressProperties{}); err != nil {
t.Fatalf("AddProtocolAddress(%d, %#v, {}): %s", NICID, protocolAddrV4, err)
}
protocolAddrV6 := tcpip.ProtocolAddress{
Protocol: ipv6.ProtocolNumber,
AddressWithPrefix: tcpip.Address(StackV6Addr).WithPrefix(),
}
if err := s.AddProtocolAddress(NICID, protocolAddrV6, stack.AddressProperties{}); err != nil {
t.Fatalf("AddProtocolAddress(%d, %#v, {}): %s", NICID, protocolAddrV6, err)
}
s.SetRouteTable([]tcpip.Route{
{
Destination: header.IPv4EmptySubnet,
NIC: NICID,
},
{
Destination: header.IPv6EmptySubnet,
NIC: NICID,
},
})
return &Context{
T: t,
Stack: s,
LinkEP: ep,
}
}
// Cleanup closes the context endpoint if required.
func (c *Context) Cleanup() {
_ = c.LinkEP.Drain()
if c.EP != nil {
c.EP.Close()
}
c.Stack.Destroy()
c.Stack = nil
refs.DoRepeatedLeakCheck()
}
// CreateEndpoint creates the Context's Endpoint.
func (c *Context) CreateEndpoint(network tcpip.NetworkProtocolNumber, transport tcpip.TransportProtocolNumber) {
c.T.Helper()
var err tcpip.Error
c.EP, err = c.Stack.NewEndpoint(transport, network, &c.WQ)
if err != nil {
c.T.Fatalf("c.Stack.NewEndpoint(%d, %d, _) failed: %s", transport, network, err)
}
}
// CreateEndpointForFlow creates the Context's Endpoint and configured it
// according to the given TestFlow.
func (c *Context) CreateEndpointForFlow(flow TestFlow, transport tcpip.TransportProtocolNumber) {
c.T.Helper()
c.CreateEndpoint(flow.SockProto(), transport)
if flow.isV6Only() {
c.EP.SocketOptions().SetV6Only(true)
} else if flow.isBroadcast() {
c.EP.SocketOptions().SetBroadcast(true)
}
}
// CreateRawEndpoint creates the Context's Endpoint.
func (c *Context) CreateRawEndpoint(network tcpip.NetworkProtocolNumber, transport tcpip.TransportProtocolNumber) {
c.T.Helper()
var err tcpip.Error
c.EP, err = c.Stack.NewRawEndpoint(transport, network, &c.WQ, true /* associated */)
if err != nil {
c.T.Fatal("c.Stack.NewRawEndpoint failed: ", err)
}
}
// CreateRawEndpointForFlow creates the Context's Endpoint and configured it
// according to the given TestFlow.
func (c *Context) CreateRawEndpointForFlow(flow TestFlow, transport tcpip.TransportProtocolNumber) {
c.T.Helper()
c.CreateRawEndpoint(flow.SockProto(), transport)
if flow.isV6Only() {
c.EP.SocketOptions().SetV6Only(true)
} else if flow.isBroadcast() {
c.EP.SocketOptions().SetBroadcast(true)
}
}
// CheckEndpointWriteStats checks that the write statistic related to the given
// error has been incremented as expected.
func (c *Context) CheckEndpointWriteStats(incr uint64, want *tcpip.TransportEndpointStats, err tcpip.Error) {
var got tcpip.TransportEndpointStats
c.EP.Stats().(*tcpip.TransportEndpointStats).Clone(&got)
switch err.(type) {
case nil:
want.PacketsSent.IncrementBy(incr)
case *tcpip.ErrMessageTooLong, *tcpip.ErrInvalidOptionValue:
want.WriteErrors.InvalidArgs.IncrementBy(incr)
case *tcpip.ErrClosedForSend:
want.WriteErrors.WriteClosed.IncrementBy(incr)
case *tcpip.ErrInvalidEndpointState:
want.WriteErrors.InvalidEndpointState.IncrementBy(incr)
case *tcpip.ErrHostUnreachable, *tcpip.ErrBroadcastDisabled, *tcpip.ErrNetworkUnreachable:
want.SendErrors.NoRoute.IncrementBy(incr)
default:
want.SendErrors.SendToNetworkFailed.IncrementBy(incr)
}
if !reflect.DeepEqual(&got, want) {
c.T.Errorf("Endpoint stats not matching for error %s: got %#v, want %#v", err, &got, want)
}
}
// CheckEndpointReadStats checks that the read statistic related to the given
// error has been incremented as expected.
func (c *Context) CheckEndpointReadStats(incr uint64, want *tcpip.TransportEndpointStats, err tcpip.Error) {
c.T.Helper()
var got tcpip.TransportEndpointStats
c.EP.Stats().(*tcpip.TransportEndpointStats).Clone(&got)
switch err.(type) {
case nil, *tcpip.ErrWouldBlock:
case *tcpip.ErrClosedForReceive:
want.ReadErrors.ReadClosed.IncrementBy(incr)
default:
c.T.Errorf("Endpoint error missing stats update for err %s", err)
}
if !reflect.DeepEqual(&got, want) {
c.T.Errorf("Endpoint stats not matching for error %s: got %#v, want %#v", err, &got, want)
}
}
// InjectPacket injects a packet into the context's link endpoint.
func (c *Context) InjectPacket(netProto tcpip.NetworkProtocolNumber, buf []byte) {
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(buf),
})
defer pkt.DecRef()
c.LinkEP.InjectInbound(netProto, pkt)
}
// readExpectations holds information about the expected outcome when reading
// from the context's endpoint.
type readExpectations struct {
nothingToRead bool
payload []byte
addresses Header4Tuple
readShouldFail bool
}
// readFromEndpoint attempts to read a packet from the endpoint and compares the
// outcome with the given expectations.
func (c *Context) readFromEndpoint(expectations readExpectations, checkers ...checker.ControlMessagesChecker) {
c.T.Helper()
// Try to receive the data.
we, ch := waiter.NewChannelEntry(waiter.ReadableEvents)
c.WQ.EventRegister(&we)
defer c.WQ.EventUnregister(&we)
// Take a snapshot of the stats to validate them at the end of the test.
var epstats tcpip.TransportEndpointStats
c.EP.Stats().(*tcpip.TransportEndpointStats).Clone(&epstats)
var buf bytes.Buffer
res, err := c.EP.Read(&buf, tcpip.ReadOptions{NeedRemoteAddr: true})
if _, ok := err.(*tcpip.ErrWouldBlock); ok {
select {
case <-ch:
res, err = c.EP.Read(&buf, tcpip.ReadOptions{NeedRemoteAddr: true})
default:
if expectations.nothingToRead {
return
}
c.T.Fatal("timed out waiting for data")
}
}
if expectations.readShouldFail && err != nil {
c.CheckEndpointReadStats(1, &epstats, err)
return
}
if err != nil {
c.T.Fatal("Read failed:", err)
}
if expectations.nothingToRead {
c.T.Fatalf("Read unexpectedly received data from %s", res.RemoteAddr.Addr)
}
// Check the read result.
if diff := cmp.Diff(tcpip.ReadResult{
Count: buf.Len(),
Total: buf.Len(),
RemoteAddr: tcpip.FullAddress{Addr: expectations.addresses.Src.Addr},
}, res, checker.IgnoreCmpPath(
"ControlMessages", // ControlMessages are checked below.
"RemoteAddr.NIC",
"RemoteAddr.Port",
)); diff != "" {
c.T.Fatalf("Read: unexpected result (-want +got):\n%s", diff)
}
// Check the payload.
v := buf.Bytes()
if !bytes.Equal(expectations.payload, v) {
c.T.Fatalf("got payload = %x, want = %x", v, expectations.payload)
}
// Run any checkers against the ControlMessages.
for _, f := range checkers {
f(c.T, res.ControlMessages)
}
c.CheckEndpointReadStats(1, &epstats, err)
}
// ReadFromEndpointExpectSuccess attempts to reads from the endpoint and
// performs checks on the received packet, according to the given flow and
// checkers.
func (c *Context) ReadFromEndpointExpectSuccess(payload []byte, flow TestFlow, checkers ...checker.ControlMessagesChecker) {
c.T.Helper()
c.readFromEndpoint(readExpectations{
payload: payload,
addresses: flow.MakeHeader4Tuple(Incoming),
}, checkers...)
}
// ReadFromEndpointExpectNoPacket reads from the endpoint and checks that no
// packets was received.
func (c *Context) ReadFromEndpointExpectNoPacket() {
c.T.Helper()
c.readFromEndpoint(readExpectations{
nothingToRead: true,
})
}
// ReadFromEndpointExpectError reads from the endpoint and checks that an
// error was returned.
func (c *Context) ReadFromEndpointExpectError() {
c.T.Helper()
c.readFromEndpoint(readExpectations{
readShouldFail: true,
})
}