mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
376 lines
11 KiB
Go
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,
|
|
})
|
|
}
|