// 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, }) }