From 121af37738525a629ecc11863b7454b67c0f4117 Mon Sep 17 00:00:00 2001 From: Sam Balana Date: Thu, 27 May 2021 16:16:29 -0700 Subject: Support SO_BINDTODEVICE in ICMP sockets Adds support for the SO_BINDTODEVICE socket option in ICMP sockets with an accompanying packetimpact test to exercise use of this socket option. Adds a unit test to exercise the NIC selection logic introduced by this change. The remaining unit tests for ICMP sockets need to be added in a subsequent CL. See https://gvisor.dev/issues/5623 for the list of remaining unit tests. Adds a "timeout" field to PacketimpactTestInfo, necessary due to the long runtime of the newly added packetimpact test. Fixes #5678 Fixes #4896 Updates #5623 Updates #5681 Updates #5763 Updates #5956 Updates #5966 Updates #5967 PiperOrigin-RevId: 376271581 --- pkg/tcpip/socketops.go | 5 +- pkg/tcpip/transport/icmp/BUILD | 21 +- pkg/tcpip/transport/icmp/endpoint.go | 20 +- pkg/tcpip/transport/icmp/icmp_test.go | 235 ++++++ pkg/tcpip/transport/udp/endpoint.go | 39 +- test/packetimpact/runner/defs.bzl | 13 +- test/packetimpact/testbench/connections.go | 54 +- test/packetimpact/testbench/layers.go | 7 + test/packetimpact/testbench/testbench.go | 22 +- test/packetimpact/tests/BUILD | 25 +- .../tests/generic_dgram_socket_send_recv_test.go | 786 +++++++++++++++++++++ .../packetimpact/tests/udp_send_recv_dgram_test.go | 329 --------- 12 files changed, 1167 insertions(+), 389 deletions(-) create mode 100644 pkg/tcpip/transport/icmp/icmp_test.go create mode 100644 test/packetimpact/tests/generic_dgram_socket_send_recv_test.go delete mode 100644 test/packetimpact/tests/udp_send_recv_dgram_test.go diff --git a/pkg/tcpip/socketops.go b/pkg/tcpip/socketops.go index b7c2de652..0ea85f9ed 100644 --- a/pkg/tcpip/socketops.go +++ b/pkg/tcpip/socketops.go @@ -601,9 +601,10 @@ func (so *SocketOptions) GetBindToDevice() int32 { return atomic.LoadInt32(&so.bindToDevice) } -// SetBindToDevice sets value for SO_BINDTODEVICE option. +// SetBindToDevice sets value for SO_BINDTODEVICE option. If bindToDevice is +// zero, the socket device binding is removed. func (so *SocketOptions) SetBindToDevice(bindToDevice int32) Error { - if !so.handler.HasNIC(bindToDevice) { + if bindToDevice != 0 && !so.handler.HasNIC(bindToDevice) { return &ErrUnknownDevice{} } diff --git a/pkg/tcpip/transport/icmp/BUILD b/pkg/tcpip/transport/icmp/BUILD index 7e5c79776..bbc0e3ecc 100644 --- a/pkg/tcpip/transport/icmp/BUILD +++ b/pkg/tcpip/transport/icmp/BUILD @@ -1,4 +1,4 @@ -load("//tools:defs.bzl", "go_library") +load("//tools:defs.bzl", "go_library", "go_test") load("//tools/go_generics:defs.bzl", "go_template_instance") package(licenses = ["notice"]) @@ -38,3 +38,22 @@ go_library( "//pkg/waiter", ], ) + +go_test( + name = "icmp_x_test", + size = "small", + srcs = ["icmp_test.go"], + deps = [ + ":icmp", + "//pkg/tcpip", + "//pkg/tcpip/buffer", + "//pkg/tcpip/checker", + "//pkg/tcpip/header", + "//pkg/tcpip/link/channel", + "//pkg/tcpip/link/sniffer", + "//pkg/tcpip/network/ipv4", + "//pkg/tcpip/stack", + "//pkg/tcpip/testutil", + "//pkg/waiter", + ], +) diff --git a/pkg/tcpip/transport/icmp/endpoint.go b/pkg/tcpip/transport/icmp/endpoint.go index 39f526023..fb77febcf 100644 --- a/pkg/tcpip/transport/icmp/endpoint.go +++ b/pkg/tcpip/transport/icmp/endpoint.go @@ -27,8 +27,6 @@ import ( "gvisor.dev/gvisor/pkg/waiter" ) -// TODO(https://gvisor.dev/issues/5623): Unit test this package. - // +stateify savable type icmpPacket struct { icmpPacketEntry @@ -134,7 +132,8 @@ func (e *endpoint) Close() { e.shutdownFlags = tcpip.ShutdownRead | tcpip.ShutdownWrite switch e.state { case stateBound, stateConnected: - e.stack.UnregisterTransportEndpoint([]tcpip.NetworkProtocolNumber{e.NetProto}, e.TransProto, e.ID, e, ports.Flags{}, 0 /* bindToDevice */) + bindToDevice := tcpip.NICID(e.ops.GetBindToDevice()) + e.stack.UnregisterTransportEndpoint([]tcpip.NetworkProtocolNumber{e.NetProto}, e.TransProto, e.ID, e, ports.Flags{}, bindToDevice) } // Close the receive list and drain it. @@ -305,6 +304,9 @@ func (e *endpoint) write(p tcpip.Payloader, opts tcpip.WriteOptions) (int64, tcp // Reject destination address if it goes through a different // NIC than the endpoint was bound to. nicID := to.NIC + if nicID == 0 { + nicID = tcpip.NICID(e.ops.GetBindToDevice()) + } if e.BindNICID != 0 { if nicID != 0 && nicID != e.BindNICID { return 0, &tcpip.ErrNoRoute{} @@ -349,6 +351,13 @@ func (e *endpoint) write(p tcpip.Payloader, opts tcpip.WriteOptions) (int64, tcp return int64(len(v)), nil } +var _ tcpip.SocketOptionsHandler = (*endpoint)(nil) + +// HasNIC implements tcpip.SocketOptionsHandler. +func (e *endpoint) HasNIC(id int32) bool { + return e.stack.HasNIC(tcpip.NICID(id)) +} + // SetSockOpt sets a socket option. func (*endpoint) SetSockOpt(tcpip.SettableSocketOption) tcpip.Error { return nil @@ -608,17 +617,18 @@ func (*endpoint) Accept(*tcpip.FullAddress) (tcpip.Endpoint, *waiter.Queue, tcpi } func (e *endpoint) registerWithStack(_ tcpip.NICID, netProtos []tcpip.NetworkProtocolNumber, id stack.TransportEndpointID) (stack.TransportEndpointID, tcpip.Error) { + bindToDevice := tcpip.NICID(e.ops.GetBindToDevice()) if id.LocalPort != 0 { // The endpoint already has a local port, just attempt to // register it. - err := e.stack.RegisterTransportEndpoint(netProtos, e.TransProto, id, e, ports.Flags{}, 0 /* bindToDevice */) + err := e.stack.RegisterTransportEndpoint(netProtos, e.TransProto, id, e, ports.Flags{}, bindToDevice) return id, err } // We need to find a port for the endpoint. _, err := e.stack.PickEphemeralPort(e.stack.Rand(), func(p uint16) (bool, tcpip.Error) { id.LocalPort = p - err := e.stack.RegisterTransportEndpoint(netProtos, e.TransProto, id, e, ports.Flags{}, 0 /* bindtodevice */) + err := e.stack.RegisterTransportEndpoint(netProtos, e.TransProto, id, e, ports.Flags{}, bindToDevice) switch err.(type) { case nil: return true, nil diff --git a/pkg/tcpip/transport/icmp/icmp_test.go b/pkg/tcpip/transport/icmp/icmp_test.go new file mode 100644 index 000000000..cc950cbde --- /dev/null +++ b/pkg/tcpip/transport/icmp/icmp_test.go @@ -0,0 +1,235 @@ +// Copyright 2021 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 icmp_test + +import ( + "testing" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/buffer" + "gvisor.dev/gvisor/pkg/tcpip/checker" + "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/stack" + "gvisor.dev/gvisor/pkg/tcpip/testutil" + "gvisor.dev/gvisor/pkg/tcpip/transport/icmp" + "gvisor.dev/gvisor/pkg/waiter" +) + +// TODO(https://gvisor.dev/issues/5623): Finish unit testing the icmp package. +// See the issue for remaining areas of work. + +var ( + localV4Addr1 = testutil.MustParse4("10.0.0.1") + localV4Addr2 = testutil.MustParse4("10.0.0.2") + remoteV4Addr = testutil.MustParse4("10.0.0.3") +) + +func addNICWithDefaultRoute(t *testing.T, s *stack.Stack, id tcpip.NICID, name string, addrV4 tcpip.Address) *channel.Endpoint { + t.Helper() + + ep := channel.New(1 /* size */, header.IPv4MinimumMTU, "" /* linkAddr */) + t.Cleanup(ep.Close) + + wep := stack.LinkEndpoint(ep) + if testing.Verbose() { + wep = sniffer.New(ep) + } + + opts := stack.NICOptions{Name: name} + if err := s.CreateNICWithOptions(id, wep, opts); err != nil { + t.Fatalf("s.CreateNIC(%d, _) = %s", id, err) + } + + if err := s.AddAddress(id, ipv4.ProtocolNumber, addrV4); err != nil { + t.Fatalf("s.AddAddress(%d, %d, %s) = %s", id, ipv4.ProtocolNumber, addrV4, err) + } + + s.AddRoute(tcpip.Route{ + Destination: header.IPv4EmptySubnet, + NIC: id, + }) + + return ep +} + +func writePayload(buf []byte) { + for i := range buf { + buf[i] = byte(i) + } +} + +func newICMPv4EchoRequest(payloadSize uint32) buffer.View { + buf := buffer.NewView(header.ICMPv4MinimumSize + int(payloadSize)) + writePayload(buf[header.ICMPv4MinimumSize:]) + + icmp := header.ICMPv4(buf) + icmp.SetType(header.ICMPv4Echo) + // No need to set the checksum; it is reset by the socket before the packet + // is sent. + + return buf +} + +// TestWriteUnboundWithBindToDevice exercises writing to an unbound ICMP socket +// when SO_BINDTODEVICE is set to the non-default NIC for that subnet. +// +// Only IPv4 is tested. The logic to determine which NIC to use is agnostic to +// the version of IP. +func TestWriteUnboundWithBindToDevice(t *testing.T) { + s := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{icmp.NewProtocol4}, + HandleLocal: true, + }) + + // Add two NICs, both with default routes on the same subnet. The first NIC + // added will be the default NIC for that subnet. + defaultEP := addNICWithDefaultRoute(t, s, 1, "default", localV4Addr1) + alternateEP := addNICWithDefaultRoute(t, s, 2, "alternate", localV4Addr2) + + socket, err := s.NewEndpoint(icmp.ProtocolNumber4, ipv4.ProtocolNumber, &waiter.Queue{}) + if err != nil { + t.Fatalf("s.NewEndpoint(%d, %d, _) = %s", icmp.ProtocolNumber4, ipv4.ProtocolNumber, err) + } + defer socket.Close() + + echoPayloadSize := defaultEP.MTU() - header.IPv4MinimumSize - header.ICMPv4MinimumSize + + // Send a packet without SO_BINDTODEVICE. This verifies that the first NIC + // to be added is the default NIC to send packets when not explicitly bound. + { + buf := newICMPv4EchoRequest(echoPayloadSize) + r := buf.Reader() + n, err := socket.Write(&r, tcpip.WriteOptions{ + To: &tcpip.FullAddress{Addr: remoteV4Addr}, + }) + if err != nil { + t.Fatalf("socket.Write(_, {To:%s}) = %s", remoteV4Addr, err) + } + if n != int64(len(buf)) { + t.Fatalf("got n = %d, want n = %d", n, len(buf)) + } + + // Verify the packet was sent out the default NIC. + p, ok := defaultEP.Read() + if !ok { + t.Fatalf("got defaultEP.Read(_) = _, false; want = _, true (packet wasn't written out)") + } + + vv := buffer.NewVectorisedView(p.Pkt.Size(), p.Pkt.Views()) + b := vv.ToView() + + checker.IPv4(t, b, []checker.NetworkChecker{ + checker.SrcAddr(localV4Addr1), + checker.DstAddr(remoteV4Addr), + checker.ICMPv4( + checker.ICMPv4Type(header.ICMPv4Echo), + checker.ICMPv4Payload(buf[header.ICMPv4MinimumSize:]), + ), + }...) + + // Verify the packet was not sent out the alternate NIC. + if p, ok := alternateEP.Read(); ok { + t.Fatalf("got alternateEP.Read(_) = %+v, true; want = _, false", p) + } + } + + // Send a packet with SO_BINDTODEVICE. This exercises reliance on + // SO_BINDTODEVICE to route the packet to the alternate NIC. + { + // Use SO_BINDTODEVICE to send over the alternate NIC by default. + socket.SocketOptions().SetBindToDevice(2) + + buf := newICMPv4EchoRequest(echoPayloadSize) + r := buf.Reader() + n, err := socket.Write(&r, tcpip.WriteOptions{ + To: &tcpip.FullAddress{Addr: remoteV4Addr}, + }) + if err != nil { + t.Fatalf("socket.Write(_, {To:%s}) = %s", tcpip.Address(remoteV4Addr), err) + } + if n != int64(len(buf)) { + t.Fatalf("got n = %d, want n = %d", n, len(buf)) + } + + // Verify the packet was not sent out the default NIC. + if p, ok := defaultEP.Read(); ok { + t.Fatalf("got defaultEP.Read(_) = %+v, true; want = _, false", p) + } + + // Verify the packet was sent out the alternate NIC. + p, ok := alternateEP.Read() + if !ok { + t.Fatalf("got alternateEP.Read(_) = _, false; want = _, true (packet wasn't written out)") + } + + vv := buffer.NewVectorisedView(p.Pkt.Size(), p.Pkt.Views()) + b := vv.ToView() + + checker.IPv4(t, b, []checker.NetworkChecker{ + checker.SrcAddr(localV4Addr2), + checker.DstAddr(remoteV4Addr), + checker.ICMPv4( + checker.ICMPv4Type(header.ICMPv4Echo), + checker.ICMPv4Payload(buf[header.ICMPv4MinimumSize:]), + ), + }...) + } + + // Send a packet with SO_BINDTODEVICE cleared. This verifies that clearing + // the device binding will fallback to using the default NIC to send + // packets. + { + socket.SocketOptions().SetBindToDevice(0) + + buf := newICMPv4EchoRequest(echoPayloadSize) + r := buf.Reader() + n, err := socket.Write(&r, tcpip.WriteOptions{ + To: &tcpip.FullAddress{Addr: remoteV4Addr}, + }) + if err != nil { + t.Fatalf("socket.Write(_, {To:%s}) = %s", tcpip.Address(remoteV4Addr), err) + } + if n != int64(len(buf)) { + t.Fatalf("got n = %d, want n = %d", n, len(buf)) + } + + // Verify the packet was sent out the default NIC. + p, ok := defaultEP.Read() + if !ok { + t.Fatalf("got defaultEP.Read(_) = _, false; want = _, true (packet wasn't written out)") + } + + vv := buffer.NewVectorisedView(p.Pkt.Size(), p.Pkt.Views()) + b := vv.ToView() + + checker.IPv4(t, b, []checker.NetworkChecker{ + checker.SrcAddr(localV4Addr1), + checker.DstAddr(remoteV4Addr), + checker.ICMPv4( + checker.ICMPv4Type(header.ICMPv4Echo), + checker.ICMPv4Payload(buf[header.ICMPv4MinimumSize:]), + ), + }...) + + // Verify the packet was not sent out the alternate NIC. + if p, ok := alternateEP.Read(); ok { + t.Fatalf("got alternateEP.Read(_) = %+v, true; want = _, false", p) + } + } +} diff --git a/pkg/tcpip/transport/udp/endpoint.go b/pkg/tcpip/transport/udp/endpoint.go index b964e446b..def9d7186 100644 --- a/pkg/tcpip/transport/udp/endpoint.go +++ b/pkg/tcpip/transport/udp/endpoint.go @@ -54,7 +54,7 @@ const ( StateClosed ) -// String implements fmt.Stringer.String. +// String implements fmt.Stringer. func (s EndpointState) String() string { switch s { case StateInitial: @@ -214,7 +214,7 @@ func (e *endpoint) EndpointState() EndpointState { return EndpointState(atomic.LoadUint32(&e.state)) } -// UniqueID implements stack.TransportEndpoint.UniqueID. +// UniqueID implements stack.TransportEndpoint. func (e *endpoint) UniqueID() uint64 { return e.uniqueID } @@ -228,14 +228,14 @@ func (e *endpoint) LastError() tcpip.Error { return err } -// UpdateLastError implements tcpip.SocketOptionsHandler.UpdateLastError. +// UpdateLastError implements tcpip.SocketOptionsHandler. func (e *endpoint) UpdateLastError(err tcpip.Error) { e.lastErrorMu.Lock() e.lastError = err e.lastErrorMu.Unlock() } -// Abort implements stack.TransportEndpoint.Abort. +// Abort implements stack.TransportEndpoint. func (e *endpoint) Abort() { e.Close() } @@ -291,10 +291,10 @@ func (e *endpoint) Close() { e.waiterQueue.Notify(waiter.EventHUp | waiter.EventErr | waiter.ReadableEvents | waiter.WritableEvents) } -// ModerateRecvBuf implements tcpip.Endpoint.ModerateRecvBuf. +// ModerateRecvBuf implements tcpip.Endpoint. func (*endpoint) ModerateRecvBuf(int) {} -// Read implements tcpip.Endpoint.Read. +// Read implements tcpip.Endpoint. func (e *endpoint) Read(dst io.Writer, opts tcpip.ReadOptions) (tcpip.ReadResult, tcpip.Error) { if err := e.LastError(); err != nil { return tcpip.ReadResult{}, err @@ -583,21 +583,21 @@ func (e *endpoint) write(p tcpip.Payloader, opts tcpip.WriteOptions) (int64, tcp return int64(len(v)), nil } -// OnReuseAddressSet implements tcpip.SocketOptionsHandler.OnReuseAddressSet. +// OnReuseAddressSet implements tcpip.SocketOptionsHandler. func (e *endpoint) OnReuseAddressSet(v bool) { e.mu.Lock() e.portFlags.MostRecent = v e.mu.Unlock() } -// OnReusePortSet implements tcpip.SocketOptionsHandler.OnReusePortSet. +// OnReusePortSet implements tcpip.SocketOptionsHandler. func (e *endpoint) OnReusePortSet(v bool) { e.mu.Lock() e.portFlags.LoadBalanced = v e.mu.Unlock() } -// SetSockOptInt implements tcpip.Endpoint.SetSockOptInt. +// SetSockOptInt implements tcpip.Endpoint. func (e *endpoint) SetSockOptInt(opt tcpip.SockOptInt, v int) tcpip.Error { switch opt { case tcpip.MTUDiscoverOption: @@ -631,11 +631,14 @@ func (e *endpoint) SetSockOptInt(opt tcpip.SockOptInt, v int) tcpip.Error { return nil } +var _ tcpip.SocketOptionsHandler = (*endpoint)(nil) + +// HasNIC implements tcpip.SocketOptionsHandler. func (e *endpoint) HasNIC(id int32) bool { - return id == 0 || e.stack.HasNIC(tcpip.NICID(id)) + return e.stack.HasNIC(tcpip.NICID(id)) } -// SetSockOpt implements tcpip.Endpoint.SetSockOpt. +// SetSockOpt implements tcpip.Endpoint. func (e *endpoint) SetSockOpt(opt tcpip.SettableSocketOption) tcpip.Error { switch v := opt.(type) { case *tcpip.MulticastInterfaceOption: @@ -751,7 +754,7 @@ func (e *endpoint) SetSockOpt(opt tcpip.SettableSocketOption) tcpip.Error { return nil } -// GetSockOptInt implements tcpip.Endpoint.GetSockOptInt. +// GetSockOptInt implements tcpip.Endpoint. func (e *endpoint) GetSockOptInt(opt tcpip.SockOptInt) (int, tcpip.Error) { switch opt { case tcpip.IPv4TOSOption: @@ -797,7 +800,7 @@ func (e *endpoint) GetSockOptInt(opt tcpip.SockOptInt) (int, tcpip.Error) { } } -// GetSockOpt implements tcpip.Endpoint.GetSockOpt. +// GetSockOpt implements tcpip.Endpoint. func (e *endpoint) GetSockOpt(opt tcpip.GettableSocketOption) tcpip.Error { switch o := opt.(type) { case *tcpip.MulticastInterfaceOption: @@ -874,7 +877,7 @@ func (e *endpoint) checkV4MappedLocked(addr tcpip.FullAddress) (tcpip.FullAddres return unwrapped, netProto, nil } -// Disconnect implements tcpip.Endpoint.Disconnect. +// Disconnect implements tcpip.Endpoint. func (e *endpoint) Disconnect() tcpip.Error { e.mu.Lock() defer e.mu.Unlock() @@ -1388,7 +1391,7 @@ func (e *endpoint) HandleError(transErr stack.TransportError, pkt *stack.PacketB } } -// State implements tcpip.Endpoint.State. +// State implements tcpip.Endpoint. func (e *endpoint) State() uint32 { return uint32(e.EndpointState()) } @@ -1407,19 +1410,19 @@ func (e *endpoint) Stats() tcpip.EndpointStats { return &e.stats } -// Wait implements tcpip.Endpoint.Wait. +// Wait implements tcpip.Endpoint. func (*endpoint) Wait() {} func (e *endpoint) isBroadcastOrMulticast(nicID tcpip.NICID, netProto tcpip.NetworkProtocolNumber, addr tcpip.Address) bool { return addr == header.IPv4Broadcast || header.IsV4MulticastAddress(addr) || header.IsV6MulticastAddress(addr) || e.stack.IsSubnetBroadcast(nicID, netProto, addr) } -// SetOwner implements tcpip.Endpoint.SetOwner. +// SetOwner implements tcpip.Endpoint. func (e *endpoint) SetOwner(owner tcpip.PacketOwner) { e.owner = owner } -// SocketOptions implements tcpip.Endpoint.SocketOptions. +// SocketOptions implements tcpip.Endpoint. func (e *endpoint) SocketOptions() *tcpip.SocketOptions { return &e.ops } diff --git a/test/packetimpact/runner/defs.bzl b/test/packetimpact/runner/defs.bzl index afe73a69a..19f6cc0e0 100644 --- a/test/packetimpact/runner/defs.bzl +++ b/test/packetimpact/runner/defs.bzl @@ -115,7 +115,7 @@ def packetimpact_netstack_test( **kwargs ) -def packetimpact_go_test(name, expect_native_failure = False, expect_netstack_failure = False, num_duts = 1): +def packetimpact_go_test(name, expect_native_failure = False, expect_netstack_failure = False, num_duts = 1, **kwargs): """Add packetimpact tests written in go. Args: @@ -123,6 +123,7 @@ def packetimpact_go_test(name, expect_native_failure = False, expect_netstack_fa expect_native_failure: the test must fail natively expect_netstack_failure: the test must fail for Netstack num_duts: how many DUTs are needed for the test + **kwargs: all the other args, forwarded to packetimpact_native_test and packetimpact_netstack_test """ testbench_binary = name + "_test" packetimpact_native_test( @@ -130,12 +131,14 @@ def packetimpact_go_test(name, expect_native_failure = False, expect_netstack_fa expect_failure = expect_native_failure, num_duts = num_duts, testbench_binary = testbench_binary, + **kwargs ) packetimpact_netstack_test( name = name, expect_failure = expect_netstack_failure, num_duts = num_duts, testbench_binary = testbench_binary, + **kwargs ) def packetimpact_testbench(name, size = "small", pure = True, **kwargs): @@ -163,6 +166,7 @@ PacketimpactTestInfo = provider( doc = "Provide information for packetimpact tests", fields = [ "name", + "timeout", "expect_netstack_failure", "num_duts", ], @@ -270,9 +274,6 @@ ALL_TESTS = [ name = "ipv6_fragment_icmp_error", num_duts = 3, ), - PacketimpactTestInfo( - name = "udp_send_recv_dgram", - ), PacketimpactTestInfo( name = "tcp_linger", ), @@ -289,6 +290,10 @@ ALL_TESTS = [ PacketimpactTestInfo( name = "tcp_fin_retransmission", ), + PacketimpactTestInfo( + name = "generic_dgram_socket_send_recv", + timeout = "long", + ), ] def validate_all_tests(): diff --git a/test/packetimpact/testbench/connections.go b/test/packetimpact/testbench/connections.go index 8ad9040ff..ed56f9ac7 100644 --- a/test/packetimpact/testbench/connections.go +++ b/test/packetimpact/testbench/connections.go @@ -594,32 +594,50 @@ func (conn *Connection) Expect(t *testing.T, layer Layer, timeout time.Duration) func (conn *Connection) ExpectFrame(t *testing.T, layers Layers, timeout time.Duration) (Layers, error) { t.Helper() - deadline := time.Now().Add(timeout) + frames, ok := conn.ListenForFrame(t, layers, timeout) + if ok { + return frames[len(frames)-1], nil + } + if len(frames) == 0 { + return nil, fmt.Errorf("got no frames matching %s during %s", layers, timeout) + } + var errs error + for _, got := range frames { + want := conn.incoming(layers) + if err := want.merge(layers); err != nil { + errs = multierr.Combine(errs, err) + } else { + errs = multierr.Combine(errs, &layersError{got: got, want: want}) + } + } + return nil, fmt.Errorf("got frames:\n%w want %s during %s", errs, layers, timeout) +} + +// ListenForFrame captures all frames until a frame matches the provided Layers, +// or until the timeout specified. Returns all captured frames, including the +// matched frame, and true if the desired frame was found. +func (conn *Connection) ListenForFrame(t *testing.T, layers Layers, timeout time.Duration) ([]Layers, bool) { + t.Helper() + + deadline := time.Now().Add(timeout) + var frames []Layers for { - var gotLayers Layers + var got Layers if timeout := time.Until(deadline); timeout > 0 { - gotLayers = conn.recvFrame(t, timeout) + got = conn.recvFrame(t, timeout) } - if gotLayers == nil { - if errs == nil { - return nil, fmt.Errorf("got no frames matching %s during %s", layers, timeout) - } - return nil, fmt.Errorf("got frames:\n%w want %s during %s", errs, layers, timeout) + if got == nil { + return frames, false } - if conn.match(layers, gotLayers) { + frames = append(frames, got) + if conn.match(layers, got) { for i, s := range conn.layerStates { - if err := s.received(gotLayers[i]); err != nil { + if err := s.received(got[i]); err != nil { t.Fatalf("failed to update test connection's layer states based on received frame: %s", err) } } - return gotLayers, nil - } - want := conn.incoming(layers) - if err := want.merge(layers); err != nil { - errs = multierr.Combine(errs, err) - } else { - errs = multierr.Combine(errs, &layersError{got: gotLayers, want: want}) + return frames, true } } } @@ -1025,6 +1043,8 @@ func (conn *UDPIPv4) SendIP(t *testing.T, ip IPv4, udp UDP, additionalLayers ... // SendFrame sends a frame on the wire and updates the state of all layers. func (conn *UDPIPv4) SendFrame(t *testing.T, overrideLayers Layers, additionalLayers ...Layer) { + t.Helper() + conn.send(t, overrideLayers, additionalLayers...) } diff --git a/test/packetimpact/testbench/layers.go b/test/packetimpact/testbench/layers.go index ef8b63db4..078f500e4 100644 --- a/test/packetimpact/testbench/layers.go +++ b/test/packetimpact/testbench/layers.go @@ -824,6 +824,7 @@ type ICMPv6 struct { Type *header.ICMPv6Type Code *header.ICMPv6Code Checksum *uint16 + Ident *uint16 // Only in Echo Request/Reply. Pointer *uint32 // Only in Parameter Problem. Payload []byte } @@ -849,6 +850,10 @@ func (l *ICMPv6) ToBytes() ([]byte, error) { } typ := h.Type() switch typ { + case header.ICMPv6EchoRequest, header.ICMPv6EchoReply: + if l.Ident != nil { + h.SetIdent(*l.Ident) + } case header.ICMPv6ParamProblem: if l.Pointer != nil { h.SetTypeSpecific(*l.Pointer) @@ -899,6 +904,8 @@ func parseICMPv6(b []byte) (Layer, layerParser) { Payload: h.Payload(), } switch msgType { + case header.ICMPv6EchoRequest, header.ICMPv6EchoReply: + icmpv6.Ident = Uint16(h.Ident()) case header.ICMPv6ParamProblem: icmpv6.Pointer = Uint32(h.TypeSpecific()) } diff --git a/test/packetimpact/testbench/testbench.go b/test/packetimpact/testbench/testbench.go index 37d02365a..d0efbcc08 100644 --- a/test/packetimpact/testbench/testbench.go +++ b/test/packetimpact/testbench/testbench.go @@ -57,11 +57,21 @@ type DUTUname struct { OperatingSystem string } -// IsLinux returns true if we are running natively on Linux. +// IsLinux returns true if the DUT is running Linux. func (n *DUTUname) IsLinux() bool { return Native && n.OperatingSystem == "GNU/Linux" } +// IsGvisor returns true if the DUT is running gVisor. +func (*DUTUname) IsGvisor() bool { + return !Native +} + +// IsFuchsia returns true if the DUT is running Fuchsia. +func (n *DUTUname) IsFuchsia() bool { + return Native && n.OperatingSystem == "Fuchsia" +} + // DUTTestNet describes the test network setup on dut and how the testbench // should connect with an existing DUT. type DUTTestNet struct { @@ -99,6 +109,16 @@ type DUTTestNet struct { POSIXServerPort uint16 } +// SubnetBroadcast returns the test network's subnet broadcast address. +func (n *DUTTestNet) SubnetBroadcast() net.IP { + addr := append([]byte(nil), n.RemoteIPv4...) + mask := net.CIDRMask(n.IPv4PrefixLength, net.IPv4len*8) + for i := range addr { + addr[i] |= ^mask[i] + } + return addr +} + // registerFlags defines flags and associates them with the package-level // exported variables above. It should be called by tests in their init // functions. diff --git a/test/packetimpact/tests/BUILD b/test/packetimpact/tests/BUILD index b1d280f98..37b59b1d9 100644 --- a/test/packetimpact/tests/BUILD +++ b/test/packetimpact/tests/BUILD @@ -305,18 +305,6 @@ packetimpact_testbench( ], ) -packetimpact_testbench( - name = "udp_send_recv_dgram", - srcs = ["udp_send_recv_dgram_test.go"], - deps = [ - "//pkg/tcpip", - "//pkg/tcpip/header", - "//test/packetimpact/testbench", - "@com_github_google_go_cmp//cmp:go_default_library", - "@org_golang_x_sys//unix:go_default_library", - ], -) - packetimpact_testbench( name = "tcp_linger", srcs = ["tcp_linger_test.go"], @@ -410,10 +398,23 @@ packetimpact_testbench( ], ) +packetimpact_testbench( + name = "generic_dgram_socket_send_recv", + srcs = ["generic_dgram_socket_send_recv_test.go"], + deps = [ + "//pkg/tcpip", + "//pkg/tcpip/header", + "//test/packetimpact/testbench", + "@com_github_google_go_cmp//cmp:go_default_library", + "@org_golang_x_sys//unix:go_default_library", + ], +) + validate_all_tests() [packetimpact_go_test( name = t.name, + timeout = t.timeout if hasattr(t, "timeout") else "moderate", expect_netstack_failure = hasattr(t, "expect_netstack_failure"), num_duts = t.num_duts if hasattr(t, "num_duts") else 1, ) for t in ALL_TESTS] diff --git a/test/packetimpact/tests/generic_dgram_socket_send_recv_test.go b/test/packetimpact/tests/generic_dgram_socket_send_recv_test.go new file mode 100644 index 000000000..00e0f7690 --- /dev/null +++ b/test/packetimpact/tests/generic_dgram_socket_send_recv_test.go @@ -0,0 +1,786 @@ +// Copyright 2021 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 generic_dgram_socket_send_recv_test + +import ( + "context" + "flag" + "fmt" + "net" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/test/packetimpact/testbench" +) + +const ( + // Even though sockets allow larger datagrams we don't test it here as they + // need to be fragmented and written out as individual frames. + + maxICMPv4PayloadSize = header.IPv4MinimumMTU - header.EthernetMinimumSize - header.IPv4MinimumSize - header.ICMPv4MinimumSize + maxICMPv6PayloadSize = header.IPv6MinimumMTU - header.EthernetMinimumSize - header.IPv6MinimumSize - header.ICMPv6MinimumSize + maxUDPv4PayloadSize = header.IPv4MinimumMTU - header.EthernetMinimumSize - header.IPv4MinimumSize - header.UDPMinimumSize + maxUDPv6PayloadSize = header.IPv6MinimumMTU - header.EthernetMinimumSize - header.IPv6MinimumSize - header.UDPMinimumSize +) + +func maxUDPPayloadSize(addr net.IP) int { + if addr.To4() != nil { + return maxUDPv4PayloadSize + } + return maxUDPv6PayloadSize +} + +func init() { + testbench.Initialize(flag.CommandLine) + testbench.RPCTimeout = 500 * time.Millisecond +} + +func expectedEthLayer(t *testing.T, dut testbench.DUT, socketFD int32, sendTo net.IP) testbench.Layer { + t.Helper() + dst := func() tcpip.LinkAddress { + if isBroadcast(dut, sendTo) { + dut.SetSockOptInt(t, socketFD, unix.SOL_SOCKET, unix.SO_BROADCAST, 1) + + // When sending to broadcast (subnet or limited), the expected ethernet + // address is also broadcast. + return header.EthernetBroadcastAddress + } + if sendTo.IsMulticast() { + if sendTo4 := sendTo.To4(); sendTo4 != nil { + return header.EthernetAddressFromMulticastIPv4Address(tcpip.Address(sendTo4)) + } + return header.EthernetAddressFromMulticastIPv6Address(tcpip.Address(sendTo.To16())) + } + return "" + }() + var ether testbench.Ether + if len(dst) != 0 { + ether.DstAddr = &dst + } + return ðer +} + +type protocolTest interface { + Name() string + Send(t *testing.T, dut testbench.DUT, bindTo, sendTo net.IP, bindToDevice bool) + Receive(t *testing.T, dut testbench.DUT, bindTo, sendTo net.IP, bindToDevice bool) +} + +func TestSocket(t *testing.T) { + dut := testbench.NewDUT(t) + subnetBroadcast := dut.Net.SubnetBroadcast() + + for _, proto := range []protocolTest{ + &icmpV4Test{}, + &icmpV6Test{}, + &udpTest{}, + } { + t.Run(proto.Name(), func(t *testing.T) { + // Test every combination of bound/unbound, broadcast/multicast/unicast + // bound/destination address, and bound/not-bound to device. + for _, bindTo := range []net.IP{ + nil, // Do not bind. + net.IPv4zero, + net.IPv4bcast, + net.IPv4allsys, + net.IPv6zero, + subnetBroadcast, + dut.Net.RemoteIPv4, + dut.Net.RemoteIPv6, + } { + t.Run(fmt.Sprintf("bindTo=%s", bindTo), func(t *testing.T) { + for _, sendTo := range []net.IP{ + net.IPv4bcast, + net.IPv4allsys, + subnetBroadcast, + dut.Net.LocalIPv4, + dut.Net.LocalIPv6, + dut.Net.RemoteIPv4, + dut.Net.RemoteIPv6, + } { + t.Run(fmt.Sprintf("sendTo=%s", sendTo), func(t *testing.T) { + for _, bindToDevice := range []bool{true, false} { + t.Run(fmt.Sprintf("bindToDevice=%t", bindToDevice), func(t *testing.T) { + t.Run("Send", func(t *testing.T) { + proto.Send(t, dut, bindTo, sendTo, bindToDevice) + }) + t.Run("Receive", func(t *testing.T) { + proto.Receive(t, dut, bindTo, sendTo, bindToDevice) + }) + }) + } + }) + } + }) + } + }) + } +} + +type icmpV4TestEnv struct { + socketFD int32 + ident uint16 + conn testbench.IPv4Conn + layers testbench.Layers +} + +type icmpV4Test struct{} + +func (test *icmpV4Test) setup(t *testing.T, dut testbench.DUT, bindTo, sendTo net.IP, bindToDevice bool) icmpV4TestEnv { + t.Helper() + + // Tell the DUT to create a socket. + var socketFD int32 + var ident uint16 + + if bindTo != nil { + socketFD, ident = dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_ICMP, bindTo) + } else { + // An unbound socket will auto-bind to INADDR_ANY. + socketFD = dut.Socket(t, unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_ICMP) + } + t.Cleanup(func() { + dut.Close(t, socketFD) + }) + + if bindToDevice { + dut.SetSockOpt(t, socketFD, unix.SOL_SOCKET, unix.SO_BINDTODEVICE, []byte(dut.Net.RemoteDevName)) + } + + // Create a socket on the test runner. + conn := dut.Net.NewIPv4Conn(t, testbench.IPv4{}, testbench.IPv4{}) + t.Cleanup(func() { + conn.Close(t) + }) + + return icmpV4TestEnv{ + socketFD: socketFD, + ident: ident, + conn: conn, + layers: testbench.Layers{ + expectedEthLayer(t, dut, socketFD, sendTo), + &testbench.IPv4{ + DstAddr: testbench.Address(tcpip.Address(sendTo.To4())), + }, + }, + } +} + +var _ protocolTest = (*icmpV4Test)(nil) + +func (*icmpV4Test) Name() string { return "icmpv4" } + +func (test *icmpV4Test) Send(t *testing.T, dut testbench.DUT, bindTo, sendTo net.IP, bindToDevice bool) { + if bindTo.To4() == nil || isBroadcastOrMulticast(dut, bindTo) { + // ICMPv4 sockets cannot bind to IPv6, broadcast, or multicast + // addresses. + return + } + + isV4 := sendTo.To4() != nil + + // TODO(gvisor.dev/issue/5681): Remove this case once ICMP sockets allow + // sending to broadcast and multicast addresses. + if (dut.Uname.IsGvisor() || dut.Uname.IsFuchsia()) && isV4 && isBroadcastOrMulticast(dut, sendTo) { + // expectPacket cannot be false. In some cases the packet will send, but + // with IPv4 destination incorrectly set to RemoteIPv4. It's all bad and + // not worth the effort to create a special case when this occurs. + t.Skip("TODO(gvisor.dev/issue/5681): Allow sending to broadcast and multicast addresses with ICMP sockets.") + } + + expectPacket := isV4 && !sendTo.Equal(dut.Net.RemoteIPv4) + switch { + case bindTo.Equal(dut.Net.RemoteIPv4): + // If we're explicitly bound to an interface's unicast address, + // packets are always sent on that interface. + case bindToDevice: + // If we're explicitly bound to an interface, packets are always + // sent on that interface. + case !sendTo.Equal(net.IPv4bcast) && !sendTo.IsMulticast(): + // If we're not sending to limited broadcast or multicast, the route + // table will be consulted and packets will be sent on the correct + // interface. + default: + expectPacket = false + } + + env := test.setup(t, dut, bindTo, sendTo, bindToDevice) + + for name, payload := range map[string][]byte{ + "empty": nil, + "small": []byte("hello world"), + "random1k": testbench.GenerateRandomPayload(t, maxICMPv4PayloadSize), + } { + t.Run(name, func(t *testing.T) { + icmpLayer := &testbench.ICMPv4{ + Type: testbench.ICMPv4Type(header.ICMPv4Echo), + Payload: payload, + } + bytes, err := icmpLayer.ToBytes() + if err != nil { + t.Fatalf("icmpLayer.ToBytes() = %s", err) + } + destSockaddr := unix.SockaddrInet4{} + copy(destSockaddr.Addr[:], sendTo.To4()) + + // Tell the DUT to send a packet out the ICMP socket. + if got, want := dut.SendTo(t, env.socketFD, bytes, 0, &destSockaddr), len(bytes); int(got) != want { + t.Fatalf("got dut.SendTo = %d, want %d", got, want) + } + + // Verify the test runner received an ICMP packet with the correctly + // set "ident". + if env.ident != 0 { + icmpLayer.Ident = &env.ident + } + want := append(env.layers, icmpLayer) + if got, ok := env.conn.ListenForFrame(t, want, time.Second); !ok && expectPacket { + t.Fatalf("did not receive expected frame matching %s\nGot frames: %s", want, got) + } else if ok && !expectPacket { + matchedFrame := got[len(got)-1] + t.Fatalf("got unexpected frame matching %s\nGot frame: %s", want, matchedFrame) + } + }) + } +} + +func (test *icmpV4Test) Receive(t *testing.T, dut testbench.DUT, bindTo, sendTo net.IP, bindToDevice bool) { + if bindTo.To4() == nil || isBroadcastOrMulticast(dut, bindTo) { + // ICMPv4 sockets cannot bind to IPv6, broadcast, or multicast + // addresses. + return + } + + expectPacket := (bindTo.Equal(dut.Net.RemoteIPv4) || bindTo.Equal(net.IPv4zero)) && sendTo.Equal(dut.Net.RemoteIPv4) + + // TODO(gvisor.dev/issue/5763): Remove this if statement once gVisor + // restricts ICMP sockets to receive only from unicast addresses. + if (dut.Uname.IsGvisor() || dut.Uname.IsFuchsia()) && bindTo.Equal(net.IPv4zero) && isBroadcastOrMulticast(dut, sendTo) { + expectPacket = true + } + + env := test.setup(t, dut, bindTo, sendTo, bindToDevice) + + for name, payload := range map[string][]byte{ + "empty": nil, + "small": []byte("hello world"), + "random1k": testbench.GenerateRandomPayload(t, maxICMPv4PayloadSize), + } { + t.Run(name, func(t *testing.T) { + icmpLayer := &testbench.ICMPv4{ + Type: testbench.ICMPv4Type(header.ICMPv4EchoReply), + Payload: payload, + } + if env.ident != 0 { + icmpLayer.Ident = &env.ident + } + + // Send an ICMPv4 packet from the test runner to the DUT. + frame := env.conn.CreateFrame(t, env.layers, icmpLayer) + env.conn.SendFrame(t, frame) + + // Verify the behavior of the ICMP socket on the DUT. + if expectPacket { + payload, err := icmpLayer.ToBytes() + if err != nil { + t.Fatalf("icmpLayer.ToBytes() = %s", err) + } + + // Receive one extra byte to assert the length of the + // packet received in the case where the packet contains + // more data than expected. + len := int32(len(payload)) + 1 + got, want := dut.Recv(t, env.socketFD, len, 0), payload + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("received payload does not match sent payload, diff (-want, +got):\n%s", diff) + } + } else { + // Expected receive error, set a short receive timeout. + dut.SetSockOptTimeval( + t, + env.socketFD, + unix.SOL_SOCKET, + unix.SO_RCVTIMEO, + &unix.Timeval{ + Sec: 1, + Usec: 0, + }, + ) + ret, recvPayload, errno := dut.RecvWithErrno(context.Background(), t, env.socketFD, maxICMPv4PayloadSize, 0) + if errno != unix.EAGAIN || errno != unix.EWOULDBLOCK { + t.Errorf("Recv got unexpected result, ret=%d, payload=%q, errno=%s", ret, recvPayload, errno) + } + } + }) + } +} + +type icmpV6TestEnv struct { + socketFD int32 + ident uint16 + conn testbench.IPv6Conn + layers testbench.Layers +} + +// icmpV6Test and icmpV4Test look substantially similar at first look, but have +// enough subtle differences in setup and test expectations to discourage +// refactoring: +// - Different IP layers +// - Different testbench.Connections +// - Different UNIX domain and proto arguments +// - Different expectPacket and wantErrno for send and receive +type icmpV6Test struct{} + +func (test *icmpV6Test) setup(t *testing.T, dut testbench.DUT, bindTo, sendTo net.IP, bindToDevice bool) icmpV6TestEnv { + t.Helper() + + // Tell the DUT to create a socket. + var socketFD int32 + var ident uint16 + + if bindTo != nil { + socketFD, ident = dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_ICMPV6, bindTo) + } else { + // An unbound socket will auto-bind to IN6ADDR_ANY_INIT. + socketFD = dut.Socket(t, unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_ICMPV6) + } + t.Cleanup(func() { + dut.Close(t, socketFD) + }) + + if bindToDevice { + dut.SetSockOpt(t, socketFD, unix.SOL_SOCKET, unix.SO_BINDTODEVICE, []byte(dut.Net.RemoteDevName)) + } + + // Create a socket on the test runner. + conn := dut.Net.NewIPv6Conn(t, testbench.IPv6{}, testbench.IPv6{}) + t.Cleanup(func() { + conn.Close(t) + }) + + return icmpV6TestEnv{ + socketFD: socketFD, + ident: ident, + conn: conn, + layers: testbench.Layers{ + expectedEthLayer(t, dut, socketFD, sendTo), + &testbench.IPv6{ + DstAddr: testbench.Address(tcpip.Address(sendTo.To16())), + }, + }, + } +} + +var _ protocolTest = (*icmpV6Test)(nil) + +func (*icmpV6Test) Name() string { return "icmpv6" } + +func (test *icmpV6Test) Send(t *testing.T, dut testbench.DUT, bindTo, sendTo net.IP, bindToDevice bool) { + if bindTo.To4() != nil || bindTo.IsMulticast() { + // ICMPv6 sockets cannot bind to IPv4 or multicast addresses. + return + } + + expectPacket := sendTo.Equal(dut.Net.LocalIPv6) + wantErrno := unix.Errno(0) + + if sendTo.To4() != nil { + wantErrno = unix.EINVAL + } + + // TODO(gvisor.dev/issue/5966): Remove this if statement once ICMPv6 sockets + // return EINVAL after calling sendto with an IPv4 address. + if (dut.Uname.IsGvisor() || dut.Uname.IsFuchsia()) && sendTo.To4() != nil { + switch { + case bindTo.Equal(dut.Net.RemoteIPv6): + wantErrno = unix.ENETUNREACH + case bindTo.Equal(net.IPv6zero) || bindTo == nil: + wantErrno = unix.Errno(0) + } + } + + env := test.setup(t, dut, bindTo, sendTo, bindToDevice) + + for name, payload := range map[string][]byte{ + "empty": nil, + "small": []byte("hello world"), + "random1k": testbench.GenerateRandomPayload(t, maxICMPv6PayloadSize), + } { + t.Run(name, func(t *testing.T) { + icmpLayer := &testbench.ICMPv6{ + Type: testbench.ICMPv6Type(header.ICMPv6EchoRequest), + Payload: payload, + } + bytes, err := icmpLayer.ToBytes() + if err != nil { + t.Fatalf("icmpLayer.ToBytes() = %s", err) + } + destSockaddr := unix.SockaddrInet6{ + ZoneId: dut.Net.RemoteDevID, + } + copy(destSockaddr.Addr[:], sendTo.To16()) + + // Tell the DUT to send a packet out the ICMPv6 socket. + ctx, cancel := context.WithTimeout(context.Background(), testbench.RPCTimeout) + defer cancel() + gotRet, gotErrno := dut.SendToWithErrno(ctx, t, env.socketFD, bytes, 0, &destSockaddr) + + if gotErrno != wantErrno { + t.Fatalf("got dut.SendToWithErrno(_, _, %d, _, _, %s) = (_, %s), want = (_, %s)", env.socketFD, sendTo, gotErrno, wantErrno) + } + if wantErrno != 0 { + return + } + if got, want := int(gotRet), len(bytes); got != want { + t.Fatalf("got dut.SendToWithErrno(_, _, %d, _, _, %s) = (%d, _), want = (%d, _)", env.socketFD, sendTo, got, want) + } + + // Verify the test runner received an ICMPv6 packet with the + // correctly set "ident". + if env.ident != 0 { + icmpLayer.Ident = &env.ident + } + want := append(env.layers, icmpLayer) + if got, ok := env.conn.ListenForFrame(t, want, time.Second); !ok && expectPacket { + t.Fatalf("did not receive expected frame matching %s\nGot frames: %s", want, got) + } else if ok && !expectPacket { + matchedFrame := got[len(got)-1] + t.Fatalf("got unexpected frame matching %s\nGot frame: %s", want, matchedFrame) + } + }) + } +} + +func (test *icmpV6Test) Receive(t *testing.T, dut testbench.DUT, bindTo, sendTo net.IP, bindToDevice bool) { + if bindTo.To4() != nil || bindTo.IsMulticast() { + // ICMPv6 sockets cannot bind to IPv4 or multicast addresses. + return + } + + expectPacket := true + switch { + case bindTo.Equal(dut.Net.RemoteIPv6) && sendTo.Equal(dut.Net.RemoteIPv6): + case bindTo.Equal(net.IPv6zero) && sendTo.Equal(dut.Net.RemoteIPv6): + case bindTo.Equal(net.IPv6zero) && sendTo.Equal(net.IPv6linklocalallnodes): + default: + expectPacket = false + } + + // TODO(gvisor.dev/issue/5763): Remove this if statement once gVisor + // restricts ICMP sockets to receive only from unicast addresses. + if (dut.Uname.IsGvisor() || dut.Uname.IsFuchsia()) && bindTo.Equal(net.IPv6zero) && isBroadcastOrMulticast(dut, sendTo) { + expectPacket = false + } + + env := test.setup(t, dut, bindTo, sendTo, bindToDevice) + + for name, payload := range map[string][]byte{ + "empty": nil, + "small": []byte("hello world"), + "random1k": testbench.GenerateRandomPayload(t, maxICMPv6PayloadSize), + } { + t.Run(name, func(t *testing.T) { + icmpLayer := &testbench.ICMPv6{ + Type: testbench.ICMPv6Type(header.ICMPv6EchoReply), + Payload: payload, + } + if env.ident != 0 { + icmpLayer.Ident = &env.ident + } + + // Send an ICMPv6 packet from the test runner to the DUT. + frame := env.conn.CreateFrame(t, env.layers, icmpLayer) + env.conn.SendFrame(t, frame) + + // Verify the behavior of the ICMPv6 socket on the DUT. + if expectPacket { + payload, err := icmpLayer.ToBytes() + if err != nil { + t.Fatalf("icmpLayer.ToBytes() = %s", err) + } + + // Receive one extra byte to assert the length of the + // packet received in the case where the packet contains + // more data than expected. + len := int32(len(payload)) + 1 + got, want := dut.Recv(t, env.socketFD, len, 0), payload + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("received payload does not match sent payload, diff (-want, +got):\n%s", diff) + } + } else { + // Expected receive error, set a short receive timeout. + dut.SetSockOptTimeval( + t, + env.socketFD, + unix.SOL_SOCKET, + unix.SO_RCVTIMEO, + &unix.Timeval{ + Sec: 1, + Usec: 0, + }, + ) + ret, recvPayload, errno := dut.RecvWithErrno(context.Background(), t, env.socketFD, maxICMPv6PayloadSize, 0) + if errno != unix.EAGAIN || errno != unix.EWOULDBLOCK { + t.Errorf("Recv got unexpected result, ret=%d, payload=%q, errno=%s", ret, recvPayload, errno) + } + } + }) + } +} + +type udpConn interface { + SrcPort(*testing.T) uint16 + SendFrame(*testing.T, testbench.Layers, ...testbench.Layer) + ListenForFrame(*testing.T, testbench.Layers, time.Duration) ([]testbench.Layers, bool) + Close(*testing.T) +} + +type udpTestEnv struct { + socketFD int32 + conn udpConn + layers testbench.Layers +} + +type udpTest struct{} + +func (test *udpTest) setup(t *testing.T, dut testbench.DUT, bindTo, sendTo net.IP, bindToDevice bool) udpTestEnv { + t.Helper() + + var ( + socketFD int32 + outgoingUDP, incomingUDP testbench.UDP + ) + + // Tell the DUT to create a socket. + if bindTo != nil { + var remotePort uint16 + socketFD, remotePort = dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, bindTo) + outgoingUDP.DstPort = &remotePort + incomingUDP.SrcPort = &remotePort + } else { + // An unbound socket will auto-bind to INADDR_ANY. + socketFD = dut.Socket(t, unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP) + } + t.Cleanup(func() { + dut.Close(t, socketFD) + }) + + if bindToDevice { + dut.SetSockOpt(t, socketFD, unix.SOL_SOCKET, unix.SO_BINDTODEVICE, []byte(dut.Net.RemoteDevName)) + } + + // Create a socket on the test runner. + var conn udpConn + var ipLayer testbench.Layer + if addr := sendTo.To4(); addr != nil { + udpConn := dut.Net.NewUDPIPv4(t, outgoingUDP, incomingUDP) + conn = &udpConn + ipLayer = &testbench.IPv4{ + DstAddr: testbench.Address(tcpip.Address(addr)), + } + } else { + udpConn := dut.Net.NewUDPIPv6(t, outgoingUDP, incomingUDP) + conn = &udpConn + ipLayer = &testbench.IPv6{ + DstAddr: testbench.Address(tcpip.Address(sendTo.To16())), + } + } + t.Cleanup(func() { + conn.Close(t) + }) + + return udpTestEnv{ + socketFD: socketFD, + conn: conn, + layers: testbench.Layers{ + expectedEthLayer(t, dut, socketFD, sendTo), + ipLayer, + &incomingUDP, + }, + } +} + +var _ protocolTest = (*udpTest)(nil) + +func (*udpTest) Name() string { return "udp" } + +func (test *udpTest) Send(t *testing.T, dut testbench.DUT, bindTo, sendTo net.IP, bindToDevice bool) { + canSend := bindTo == nil || bindTo.Equal(net.IPv6zero) || sameIPVersion(sendTo, bindTo) + expectPacket := canSend && !isRemoteAddr(dut, sendTo) + switch { + case bindTo.Equal(dut.Net.RemoteIPv4): + // If we're explicitly bound to an interface's unicast address, + // packets are always sent on that interface. + case bindToDevice: + // If we're explicitly bound to an interface, packets are always + // sent on that interface. + case !sendTo.Equal(net.IPv4bcast) && !sendTo.IsMulticast(): + // If we're not sending to limited broadcast, multicast, or local, the + // route table will be consulted and packets will be sent on the correct + // interface. + default: + expectPacket = false + } + + wantErrno := unix.Errno(0) + switch { + case !canSend && bindTo.To4() != nil: + wantErrno = unix.EAFNOSUPPORT + case !canSend && bindTo.To4() == nil: + wantErrno = unix.ENETUNREACH + } + + // TODO(gvisor.dev/issue/5967): Remove this if statement once UDPv4 sockets + // returns EAFNOSUPPORT after calling sendto with an IPv6 address. + if dut.Uname.IsGvisor() && !canSend && bindTo.To4() != nil { + wantErrno = unix.EINVAL + } + + env := test.setup(t, dut, bindTo, sendTo, bindToDevice) + + for name, payload := range map[string][]byte{ + "empty": nil, + "small": []byte("hello world"), + "random1k": testbench.GenerateRandomPayload(t, maxUDPPayloadSize(bindTo)), + } { + t.Run(name, func(t *testing.T) { + var destSockaddr unix.Sockaddr + if sendTo4 := sendTo.To4(); sendTo4 != nil { + addr := unix.SockaddrInet4{ + Port: int(env.conn.SrcPort(t)), + } + copy(addr.Addr[:], sendTo4) + destSockaddr = &addr + } else { + addr := unix.SockaddrInet6{ + Port: int(env.conn.SrcPort(t)), + ZoneId: dut.Net.RemoteDevID, + } + copy(addr.Addr[:], sendTo.To16()) + destSockaddr = &addr + } + + // Tell the DUT to send a packet out the UDP socket. + ctx, cancel := context.WithTimeout(context.Background(), testbench.RPCTimeout) + defer cancel() + gotRet, gotErrno := dut.SendToWithErrno(ctx, t, env.socketFD, payload, 0, destSockaddr) + + if gotErrno != wantErrno { + t.Fatalf("got dut.SendToWithErrno(_, _, %d, _, _, %s) = (_, %s), want = (_, %s)", env.socketFD, sendTo, gotErrno, wantErrno) + } + if wantErrno != 0 { + return + } + if got, want := int(gotRet), len(payload); got != want { + t.Fatalf("got dut.SendToWithErrno(_, _, %d, _, _, %s) = (%d, _), want = (%d, _)", env.socketFD, sendTo, got, want) + } + + // Verify the test runner received a UDP packet with the + // correct payload. + want := append(env.layers, &testbench.Payload{ + Bytes: payload, + }) + if got, ok := env.conn.ListenForFrame(t, want, time.Second); !ok && expectPacket { + t.Fatalf("did not receive expected frame matching %s\nGot frames: %s", want, got) + } else if ok && !expectPacket { + matchedFrame := got[len(got)-1] + t.Fatalf("got unexpected frame matching %s\nGot frame: %s", want, matchedFrame) + } + }) + } +} + +func (test *udpTest) Receive(t *testing.T, dut testbench.DUT, bindTo, sendTo net.IP, bindToDevice bool) { + subnetBroadcast := dut.Net.SubnetBroadcast() + + expectPacket := true + switch { + case bindTo.Equal(sendTo): + case bindTo.Equal(net.IPv4zero) && sameIPVersion(bindTo, sendTo) && !sendTo.Equal(dut.Net.LocalIPv4): + case bindTo.Equal(net.IPv6zero) && isBroadcast(dut, sendTo): + case bindTo.Equal(net.IPv6zero) && isRemoteAddr(dut, sendTo): + case bindTo.Equal(subnetBroadcast) && sendTo.Equal(subnetBroadcast): + default: + expectPacket = false + } + + // TODO(gvisor.dev/issue/5956): Remove this if statement once gVisor + // restricts ICMP sockets to receive only from unicast addresses. + if (dut.Uname.IsGvisor() || dut.Uname.IsFuchsia()) && bindTo.Equal(net.IPv6zero) && sendTo.Equal(net.IPv4allsys) { + expectPacket = true + } + + env := test.setup(t, dut, bindTo, sendTo, bindToDevice) + maxPayloadSize := maxUDPPayloadSize(bindTo) + + for name, payload := range map[string][]byte{ + "empty": nil, + "small": []byte("hello world"), + "random1k": testbench.GenerateRandomPayload(t, maxPayloadSize), + } { + t.Run(name, func(t *testing.T) { + // Send a UDP packet from the test runner to the DUT. + env.conn.SendFrame(t, env.layers, &testbench.Payload{Bytes: payload}) + + // Verify the behavior of the ICMP socket on the DUT. + if expectPacket { + // Receive one extra byte to assert the length of the + // packet received in the case where the packet contains + // more data than expected. + len := int32(len(payload)) + 1 + got, want := dut.Recv(t, env.socketFD, len, 0), payload + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("received payload does not match sent payload, diff (-want, +got):\n%s", diff) + } + } else { + // Expected receive error, set a short receive timeout. + dut.SetSockOptTimeval( + t, + env.socketFD, + unix.SOL_SOCKET, + unix.SO_RCVTIMEO, + &unix.Timeval{ + Sec: 1, + Usec: 0, + }, + ) + ret, recvPayload, errno := dut.RecvWithErrno(context.Background(), t, env.socketFD, int32(maxPayloadSize), 0) + if errno != unix.EAGAIN || errno != unix.EWOULDBLOCK { + t.Errorf("Recv got unexpected result, ret=%d, payload=%q, errno=%s", ret, recvPayload, errno) + } + } + }) + } +} + +func isBroadcast(dut testbench.DUT, ip net.IP) bool { + return ip.Equal(net.IPv4bcast) || ip.Equal(dut.Net.SubnetBroadcast()) +} + +func isBroadcastOrMulticast(dut testbench.DUT, ip net.IP) bool { + return isBroadcast(dut, ip) || ip.IsMulticast() +} + +func sameIPVersion(a, b net.IP) bool { + return (a.To4() == nil) == (b.To4() == nil) +} + +func isRemoteAddr(dut testbench.DUT, ip net.IP) bool { + return ip.Equal(dut.Net.RemoteIPv4) || ip.Equal(dut.Net.RemoteIPv6) +} diff --git a/test/packetimpact/tests/udp_send_recv_dgram_test.go b/test/packetimpact/tests/udp_send_recv_dgram_test.go deleted file mode 100644 index 230b012c7..000000000 --- a/test/packetimpact/tests/udp_send_recv_dgram_test.go +++ /dev/null @@ -1,329 +0,0 @@ -// Copyright 2020 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 udp_send_recv_dgram_test - -import ( - "context" - "flag" - "fmt" - "net" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "golang.org/x/sys/unix" - "gvisor.dev/gvisor/pkg/tcpip" - "gvisor.dev/gvisor/pkg/tcpip/header" - "gvisor.dev/gvisor/test/packetimpact/testbench" -) - -func init() { - testbench.Initialize(flag.CommandLine) - testbench.RPCTimeout = 500 * time.Millisecond -} - -type udpConn interface { - SrcPort(*testing.T) uint16 - SendFrame(*testing.T, testbench.Layers, ...testbench.Layer) - ExpectFrame(*testing.T, testbench.Layers, time.Duration) (testbench.Layers, error) - Close(*testing.T) -} - -type testCase struct { - bindTo, sendTo net.IP - sendToBroadcast, bindToDevice, expectData bool -} - -func TestUDP(t *testing.T) { - dut := testbench.NewDUT(t) - subnetBcast := func() net.IP { - subnet := (&tcpip.AddressWithPrefix{ - Address: tcpip.Address(dut.Net.RemoteIPv4.To4()), - PrefixLen: dut.Net.IPv4PrefixLength, - }).Subnet() - return net.IP(subnet.Broadcast()) - }() - - t.Run("Send", func(t *testing.T) { - var testCases []testCase - // Test every valid combination of bound/unbound, broadcast/multicast/unicast - // bound/destination address, and bound/not-bound to device. - for _, bindTo := range []net.IP{ - nil, // Do not bind. - net.IPv4zero, - net.IPv4bcast, - net.IPv4allsys, - subnetBcast, - dut.Net.RemoteIPv4, - dut.Net.RemoteIPv6, - } { - for _, sendTo := range []net.IP{ - net.IPv4bcast, - net.IPv4allsys, - subnetBcast, - dut.Net.LocalIPv4, - dut.Net.LocalIPv6, - } { - // Cannot send to an IPv4 address from a socket bound to IPv6 (except for IPv4-mapped IPv6), - // and viceversa. - if bindTo != nil && ((bindTo.To4() == nil) != (sendTo.To4() == nil)) { - continue - } - for _, bindToDevice := range []bool{true, false} { - expectData := true - switch { - case bindTo.Equal(dut.Net.RemoteIPv4): - // If we're explicitly bound to an interface's unicast address, - // packets are always sent on that interface. - case bindToDevice: - // If we're explicitly bound to an interface, packets are always - // sent on that interface. - case !sendTo.Equal(net.IPv4bcast) && !sendTo.IsMulticast(): - // If we're not sending to limited broadcast or multicast, the route table - // will be consulted and packets will be sent on the correct interface. - default: - expectData = false - } - testCases = append( - testCases, - testCase{ - bindTo: bindTo, - sendTo: sendTo, - sendToBroadcast: sendTo.Equal(subnetBcast) || sendTo.Equal(net.IPv4bcast), - bindToDevice: bindToDevice, - expectData: expectData, - }, - ) - } - } - } - for _, tc := range testCases { - boundTestCaseName := "unbound" - if tc.bindTo != nil { - boundTestCaseName = fmt.Sprintf("bindTo=%s", tc.bindTo) - } - t.Run(fmt.Sprintf("%s/sendTo=%s/bindToDevice=%t/expectData=%t", boundTestCaseName, tc.sendTo, tc.bindToDevice, tc.expectData), func(t *testing.T) { - runTestCase( - t, - dut, - tc, - func(t *testing.T, dut testbench.DUT, conn udpConn, socketFD int32, tc testCase, payload []byte, layers testbench.Layers) { - var destSockaddr unix.Sockaddr - if sendTo4 := tc.sendTo.To4(); sendTo4 != nil { - addr := unix.SockaddrInet4{ - Port: int(conn.SrcPort(t)), - } - copy(addr.Addr[:], sendTo4) - destSockaddr = &addr - } else { - addr := unix.SockaddrInet6{ - Port: int(conn.SrcPort(t)), - ZoneId: dut.Net.RemoteDevID, - } - copy(addr.Addr[:], tc.sendTo.To16()) - destSockaddr = &addr - } - if got, want := dut.SendTo(t, socketFD, payload, 0, destSockaddr), len(payload); int(got) != want { - t.Fatalf("got dut.SendTo = %d, want %d", got, want) - } - layers = append(layers, &testbench.Payload{ - Bytes: payload, - }) - _, err := conn.ExpectFrame(t, layers, time.Second) - - if !tc.expectData && err == nil { - t.Fatal("received unexpected packet, socket is not bound to device") - } - if err != nil && tc.expectData { - t.Fatal(err) - } - }, - ) - }) - } - }) - t.Run("Recv", func(t *testing.T) { - // Test every valid combination of broadcast/multicast/unicast - // bound/destination address, and bound/not-bound to device. - var testCases []testCase - for _, addr := range []net.IP{ - net.IPv4bcast, - net.IPv4allsys, - dut.Net.RemoteIPv4, - dut.Net.RemoteIPv6, - } { - for _, bindToDevice := range []bool{true, false} { - testCases = append( - testCases, - testCase{ - bindTo: addr, - sendTo: addr, - sendToBroadcast: addr.Equal(subnetBcast) || addr.Equal(net.IPv4bcast), - bindToDevice: bindToDevice, - expectData: true, - }, - ) - } - } - for _, bindTo := range []net.IP{ - net.IPv4zero, - subnetBcast, - dut.Net.RemoteIPv4, - } { - for _, sendTo := range []net.IP{ - subnetBcast, - net.IPv4bcast, - net.IPv4allsys, - } { - // TODO(gvisor.dev/issue/4896): Add bindTo=subnetBcast/sendTo=IPv4bcast - // and bindTo=subnetBcast/sendTo=IPv4allsys test cases. - if bindTo.Equal(subnetBcast) && (sendTo.Equal(net.IPv4bcast) || sendTo.IsMulticast()) { - continue - } - // Expect that a socket bound to a unicast address does not receive - // packets sent to an address other than the bound unicast address. - // - // Note: we cannot use net.IP.IsGlobalUnicast to test this condition - // because IsGlobalUnicast does not check whether the address is the - // subnet broadcast, and returns true in that case. - expectData := !bindTo.Equal(dut.Net.RemoteIPv4) || sendTo.Equal(dut.Net.RemoteIPv4) - for _, bindToDevice := range []bool{true, false} { - testCases = append( - testCases, - testCase{ - bindTo: bindTo, - sendTo: sendTo, - sendToBroadcast: sendTo.Equal(subnetBcast) || sendTo.Equal(net.IPv4bcast), - bindToDevice: bindToDevice, - expectData: expectData, - }, - ) - } - } - } - for _, tc := range testCases { - t.Run(fmt.Sprintf("bindTo=%s/sendTo=%s/bindToDevice=%t/expectData=%t", tc.bindTo, tc.sendTo, tc.bindToDevice, tc.expectData), func(t *testing.T) { - runTestCase( - t, - dut, - tc, - func(t *testing.T, dut testbench.DUT, conn udpConn, socketFD int32, tc testCase, payload []byte, layers testbench.Layers) { - conn.SendFrame(t, layers, &testbench.Payload{Bytes: payload}) - - if tc.expectData { - got, want := dut.Recv(t, socketFD, int32(len(payload)+1), 0), payload - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("received payload does not match sent payload, diff (-want, +got):\n%s", diff) - } - } else { - // Expected receive error, set a short receive timeout. - dut.SetSockOptTimeval( - t, - socketFD, - unix.SOL_SOCKET, - unix.SO_RCVTIMEO, - &unix.Timeval{ - Sec: 1, - Usec: 0, - }, - ) - ret, recvPayload, errno := dut.RecvWithErrno(context.Background(), t, socketFD, 100, 0) - if errno != unix.EAGAIN || errno != unix.EWOULDBLOCK { - t.Errorf("Recv got unexpected result, ret=%d, payload=%q, errno=%s", ret, recvPayload, errno) - } - } - }, - ) - }) - } - }) -} - -func runTestCase( - t *testing.T, - dut testbench.DUT, - tc testCase, - runTc func(t *testing.T, dut testbench.DUT, conn udpConn, socketFD int32, tc testCase, payload []byte, layers testbench.Layers), -) { - var ( - socketFD int32 - outgoingUDP, incomingUDP testbench.UDP - ) - if tc.bindTo != nil { - var remotePort uint16 - socketFD, remotePort = dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, tc.bindTo) - outgoingUDP.DstPort = &remotePort - incomingUDP.SrcPort = &remotePort - } else { - // An unbound socket will auto-bind to INNADDR_ANY and a random - // port on sendto. - socketFD = dut.Socket(t, unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP) - } - defer dut.Close(t, socketFD) - if tc.bindToDevice { - dut.SetSockOpt(t, socketFD, unix.SOL_SOCKET, unix.SO_BINDTODEVICE, []byte(dut.Net.RemoteDevName)) - } - - var ethernetLayer testbench.Ether - if tc.sendToBroadcast { - dut.SetSockOptInt(t, socketFD, unix.SOL_SOCKET, unix.SO_BROADCAST, 1) - - // When sending to broadcast (subnet or limited), the expected ethernet - // address is also broadcast. - ethernetBroadcastAddress := header.EthernetBroadcastAddress - ethernetLayer.DstAddr = ðernetBroadcastAddress - } else if tc.sendTo.IsMulticast() { - ethernetMulticastAddress := header.EthernetAddressFromMulticastIPv4Address(tcpip.Address(tc.sendTo.To4())) - ethernetLayer.DstAddr = ðernetMulticastAddress - } - expectedLayers := testbench.Layers{ðernetLayer} - - var conn udpConn - if sendTo4 := tc.sendTo.To4(); sendTo4 != nil { - v4Conn := dut.Net.NewUDPIPv4(t, outgoingUDP, incomingUDP) - conn = &v4Conn - expectedLayers = append( - expectedLayers, - &testbench.IPv4{ - DstAddr: testbench.Address(tcpip.Address(sendTo4)), - }, - ) - } else { - v6Conn := dut.Net.NewUDPIPv6(t, outgoingUDP, incomingUDP) - conn = &v6Conn - expectedLayers = append( - expectedLayers, - &testbench.IPv6{ - DstAddr: testbench.Address(tcpip.Address(tc.sendTo)), - }, - ) - } - defer conn.Close(t) - - expectedLayers = append(expectedLayers, &incomingUDP) - for _, v := range []struct { - name string - payload []byte - }{ - {"emptypayload", nil}, - {"small payload", []byte("hello world")}, - {"1kPayload", testbench.GenerateRandomPayload(t, 1<<10)}, - // Even though UDP allows larger dgrams we don't test it here as - // they need to be fragmented and written out as individual - // frames. - } { - runTc(t, dut, conn, socketFD, tc, v.payload, expectedLayers) - } -} -- cgit v1.2.3