diff options
Diffstat (limited to 'test/packetimpact')
-rw-r--r-- | test/packetimpact/dut/BUILD | 2 | ||||
-rw-r--r-- | test/packetimpact/dut/posix_server.cc | 40 | ||||
-rw-r--r-- | test/packetimpact/proto/posix_server.proto | 23 | ||||
-rw-r--r-- | test/packetimpact/runner/defs.bzl | 6 | ||||
-rw-r--r-- | test/packetimpact/testbench/connections.go | 40 | ||||
-rw-r--r-- | test/packetimpact/testbench/dut.go | 52 | ||||
-rw-r--r-- | test/packetimpact/tests/BUILD | 27 | ||||
-rw-r--r-- | test/packetimpact/tests/ipv4_fragment_reassembly_test.go | 12 | ||||
-rw-r--r-- | test/packetimpact/tests/tcp_info_test.go | 109 | ||||
-rw-r--r-- | test/packetimpact/tests/tcp_noaccept_close_rst_test.go | 15 | ||||
-rw-r--r-- | test/packetimpact/tests/tcp_outside_the_window_test.go | 18 | ||||
-rw-r--r-- | test/packetimpact/tests/tcp_rack_test.go | 204 | ||||
-rw-r--r-- | test/packetimpact/tests/tcp_zero_window_probe_retransmit_test.go | 28 | ||||
-rw-r--r-- | test/packetimpact/tests/udp_recv_mcast_bcast_test.go | 115 | ||||
-rw-r--r-- | test/packetimpact/tests/udp_send_recv_dgram_test.go | 338 |
15 files changed, 817 insertions, 212 deletions
diff --git a/test/packetimpact/dut/BUILD b/test/packetimpact/dut/BUILD index ccf1c735f..0be14ca3e 100644 --- a/test/packetimpact/dut/BUILD +++ b/test/packetimpact/dut/BUILD @@ -14,6 +14,7 @@ cc_binary( grpcpp, "//test/packetimpact/proto:posix_server_cc_grpc_proto", "//test/packetimpact/proto:posix_server_cc_proto", + "@com_google_absl//absl/strings:str_format", ], ) @@ -24,5 +25,6 @@ cc_binary( grpcpp, "//test/packetimpact/proto:posix_server_cc_grpc_proto", "//test/packetimpact/proto:posix_server_cc_proto", + "@com_google_absl//absl/strings:str_format", ], ) diff --git a/test/packetimpact/dut/posix_server.cc b/test/packetimpact/dut/posix_server.cc index 4de8540f6..eba21df12 100644 --- a/test/packetimpact/dut/posix_server.cc +++ b/test/packetimpact/dut/posix_server.cc @@ -16,6 +16,7 @@ #include <getopt.h> #include <netdb.h> #include <netinet/in.h> +#include <poll.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -30,6 +31,7 @@ #include "include/grpcpp/security/server_credentials.h" #include "include/grpcpp/server_builder.h" #include "include/grpcpp/server_context.h" +#include "absl/strings/str_format.h" #include "test/packetimpact/proto/posix_server.grpc.pb.h" #include "test/packetimpact/proto/posix_server.pb.h" @@ -256,6 +258,44 @@ class PosixImpl final : public posix_server::Posix::Service { return ::grpc::Status::OK; } + ::grpc::Status Poll(::grpc::ServerContext *context, + const ::posix_server::PollRequest *request, + ::posix_server::PollResponse *response) override { + std::vector<struct pollfd> pfds; + pfds.reserve(request->pfds_size()); + for (const auto &pfd : request->pfds()) { + pfds.push_back({ + .fd = pfd.fd(), + .events = static_cast<short>(pfd.events()), + }); + } + int ret = ::poll(pfds.data(), pfds.size(), request->timeout_millis()); + + response->set_ret(ret); + if (ret < 0) { + response->set_errno_(errno); + } else { + // Only pollfds that have non-empty revents are returned, the client can't + // rely on indexes of the request array. + for (const auto &pfd : pfds) { + if (pfd.revents) { + auto *proto_pfd = response->add_pfds(); + proto_pfd->set_fd(pfd.fd); + proto_pfd->set_events(pfd.revents); + } + } + if (int ready = response->pfds_size(); ret != ready) { + return ::grpc::Status( + ::grpc::StatusCode::INTERNAL, + absl::StrFormat( + "poll's return value(%d) doesn't match the number of " + "file descriptors that are actually ready(%d)", + ret, ready)); + } + } + return ::grpc::Status::OK; + } + ::grpc::Status Send(::grpc::ServerContext *context, const ::posix_server::SendRequest *request, ::posix_server::SendResponse *response) override { diff --git a/test/packetimpact/proto/posix_server.proto b/test/packetimpact/proto/posix_server.proto index f32ed54ef..b4c68764a 100644 --- a/test/packetimpact/proto/posix_server.proto +++ b/test/packetimpact/proto/posix_server.proto @@ -142,6 +142,25 @@ message ListenResponse { int32 errno_ = 2; // "errno" may fail to compile in c++. } +// The events field is overloaded: when used for request, it is copied into the +// events field of posix struct pollfd; when used for response, it is filled by +// the revents field from the posix struct pollfd. +message PollFd { + int32 fd = 1; + uint32 events = 2; +} + +message PollRequest { + repeated PollFd pfds = 1; + int32 timeout_millis = 2; +} + +message PollResponse { + int32 ret = 1; + int32 errno_ = 2; // "errno" may fail to compile in c++. + repeated PollFd pfds = 3; +} + message SendRequest { int32 sockfd = 1; bytes buf = 2; @@ -226,6 +245,10 @@ service Posix { rpc GetSockOpt(GetSockOptRequest) returns (GetSockOptResponse); // Call listen() on the DUT. rpc Listen(ListenRequest) returns (ListenResponse); + // Call poll() on the DUT. Only pollfds that have non-empty revents are + // returned, the only way to tie the response back to the original request + // is using the fd number. + rpc Poll(PollRequest) returns (PollResponse); // Call send() on the DUT. rpc Send(SendRequest) returns (SendResponse); // Call sendto() on the DUT. diff --git a/test/packetimpact/runner/defs.bzl b/test/packetimpact/runner/defs.bzl index 5c3c569de..a7c46781f 100644 --- a/test/packetimpact/runner/defs.bzl +++ b/test/packetimpact/runner/defs.bzl @@ -175,9 +175,6 @@ ALL_TESTS = [ name = "udp_discard_mcast_source_addr", ), PacketimpactTestInfo( - name = "udp_recv_mcast_bcast", - ), - PacketimpactTestInfo( name = "udp_any_addr_recv_unicast", ), PacketimpactTestInfo( @@ -281,6 +278,9 @@ ALL_TESTS = [ name = "tcp_rack", expect_netstack_failure = True, ), + PacketimpactTestInfo( + name = "tcp_info", + ), ] def validate_all_tests(): diff --git a/test/packetimpact/testbench/connections.go b/test/packetimpact/testbench/connections.go index 576577310..1453ac232 100644 --- a/test/packetimpact/testbench/connections.go +++ b/test/packetimpact/testbench/connections.go @@ -1008,6 +1008,13 @@ func (conn *UDPIPv4) LocalAddr(t *testing.T) *unix.SockaddrInet4 { return sa } +// SrcPort returns the source port of this connection. +func (conn *UDPIPv4) SrcPort(t *testing.T) uint16 { + t.Helper() + + return *conn.udpState(t).out.SrcPort +} + // Send sends a packet with reasonable defaults, potentially overriding the UDP // layer and adding additionLayers. func (conn *UDPIPv4) Send(t *testing.T, udp UDP, additionalLayers ...Layer) { @@ -1024,6 +1031,11 @@ func (conn *UDPIPv4) SendIP(t *testing.T, ip IPv4, udp UDP, additionalLayers ... (*Connection)(conn).send(t, Layers{&ip, &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) { + (*Connection)(conn).send(t, overrideLayers, additionalLayers...) +} + // Expect expects a frame with the UDP layer matching the provided UDP within // the timeout specified. If it doesn't arrive in time, an error is returned. func (conn *UDPIPv4) Expect(t *testing.T, udp UDP, timeout time.Duration) (*UDP, error) { @@ -1053,6 +1065,14 @@ func (conn *UDPIPv4) ExpectData(t *testing.T, udp UDP, payload Payload, timeout return (*Connection)(conn).ExpectFrame(t, expected, timeout) } +// ExpectFrame expects a frame that matches the provided Layers within the +// timeout specified. If it doesn't arrive in time, an error is returned. +func (conn *UDPIPv4) ExpectFrame(t *testing.T, frame Layers, timeout time.Duration) (Layers, error) { + t.Helper() + + return (*Connection)(conn).ExpectFrame(t, frame, timeout) +} + // Close frees associated resources held by the UDPIPv4 connection. func (conn *UDPIPv4) Close(t *testing.T) { t.Helper() @@ -1136,6 +1156,13 @@ func (conn *UDPIPv6) LocalAddr(t *testing.T, zoneID uint32) *unix.SockaddrInet6 return sa } +// SrcPort returns the source port of this connection. +func (conn *UDPIPv6) SrcPort(t *testing.T) uint16 { + t.Helper() + + return *conn.udpState(t).out.SrcPort +} + // Send sends a packet with reasonable defaults, potentially overriding the UDP // layer and adding additionLayers. func (conn *UDPIPv6) Send(t *testing.T, udp UDP, additionalLayers ...Layer) { @@ -1152,6 +1179,11 @@ func (conn *UDPIPv6) SendIPv6(t *testing.T, ip IPv6, udp UDP, additionalLayers . (*Connection)(conn).send(t, Layers{&ip, &udp}, additionalLayers...) } +// SendFrame sends a frame on the wire and updates the state of all layers. +func (conn *UDPIPv6) SendFrame(t *testing.T, overrideLayers Layers, additionalLayers ...Layer) { + (*Connection)(conn).send(t, overrideLayers, additionalLayers...) +} + // Expect expects a frame with the UDP layer matching the provided UDP within // the timeout specified. If it doesn't arrive in time, an error is returned. func (conn *UDPIPv6) Expect(t *testing.T, udp UDP, timeout time.Duration) (*UDP, error) { @@ -1181,6 +1213,14 @@ func (conn *UDPIPv6) ExpectData(t *testing.T, udp UDP, payload Payload, timeout return (*Connection)(conn).ExpectFrame(t, expected, timeout) } +// ExpectFrame expects a frame that matches the provided Layers within the +// timeout specified. If it doesn't arrive in time, an error is returned. +func (conn *UDPIPv6) ExpectFrame(t *testing.T, frame Layers, timeout time.Duration) (Layers, error) { + t.Helper() + + return (*Connection)(conn).ExpectFrame(t, frame, timeout) +} + // Close frees associated resources held by the UDPIPv6 connection. func (conn *UDPIPv6) Close(t *testing.T) { t.Helper() diff --git a/test/packetimpact/testbench/dut.go b/test/packetimpact/testbench/dut.go index 66a0255b8..aedcf6013 100644 --- a/test/packetimpact/testbench/dut.go +++ b/test/packetimpact/testbench/dut.go @@ -486,6 +486,56 @@ func (dut *DUT) ListenWithErrno(ctx context.Context, t *testing.T, sockfd, backl return resp.GetRet(), syscall.Errno(resp.GetErrno_()) } +// Poll calls poll on the DUT and causes a fatal test failure if it doesn't +// succeed. If more control over error handling is needed, use PollWithErrno. +// Only pollfds with non-empty revents are returned, the only way to tie the +// response back to the original request is using the fd number. +func (dut *DUT) Poll(t *testing.T, pfds []unix.PollFd, timeout time.Duration) []unix.PollFd { + t.Helper() + + ctx := context.Background() + var cancel context.CancelFunc + if timeout >= 0 { + ctx, cancel = context.WithTimeout(ctx, timeout+RPCTimeout) + defer cancel() + } + ret, result, err := dut.PollWithErrno(ctx, t, pfds, timeout) + if ret < 0 { + t.Fatalf("failed to poll: %s", err) + } + return result +} + +// PollWithErrno calls poll on the DUT. +func (dut *DUT) PollWithErrno(ctx context.Context, t *testing.T, pfds []unix.PollFd, timeout time.Duration) (int32, []unix.PollFd, error) { + t.Helper() + + req := pb.PollRequest{ + TimeoutMillis: int32(timeout.Milliseconds()), + } + for _, pfd := range pfds { + req.Pfds = append(req.Pfds, &pb.PollFd{ + Fd: pfd.Fd, + Events: uint32(pfd.Events), + }) + } + resp, err := dut.posixServer.Poll(ctx, &req) + if err != nil { + t.Fatalf("failed to call Poll: %s", err) + } + if ret, npfds := resp.GetRet(), len(resp.GetPfds()); ret >= 0 && int(ret) != npfds { + t.Fatalf("nonsensical poll response: ret(%d) != len(pfds)(%d)", ret, npfds) + } + var result []unix.PollFd + for _, protoPfd := range resp.GetPfds() { + result = append(result, unix.PollFd{ + Fd: protoPfd.GetFd(), + Revents: int16(protoPfd.GetEvents()), + }) + } + return resp.GetRet(), result, syscall.Errno(resp.GetErrno_()) +} + // Send calls send on the DUT and causes a fatal test failure if it doesn't // succeed. If more control over the timeout or error handling is needed, use // SendWithErrno. @@ -544,7 +594,7 @@ func (dut *DUT) SendToWithErrno(ctx context.Context, t *testing.T, sockfd int32, } resp, err := dut.posixServer.SendTo(ctx, &req) if err != nil { - t.Fatalf("faled to call SendTo: %s", err) + t.Fatalf("failed to call SendTo: %s", err) } return resp.GetRet(), syscall.Errno(resp.GetErrno_()) } diff --git a/test/packetimpact/tests/BUILD b/test/packetimpact/tests/BUILD index 6c6f2bdf7..baa3ae5e9 100644 --- a/test/packetimpact/tests/BUILD +++ b/test/packetimpact/tests/BUILD @@ -38,18 +38,6 @@ packetimpact_testbench( ) packetimpact_testbench( - name = "udp_recv_mcast_bcast", - srcs = ["udp_recv_mcast_bcast_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 = "udp_any_addr_recv_unicast", srcs = ["udp_any_addr_recv_unicast_test.go"], deps = [ @@ -340,6 +328,8 @@ 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", @@ -390,6 +380,19 @@ packetimpact_testbench( ], ) +packetimpact_testbench( + name = "tcp_info", + srcs = ["tcp_info_test.go"], + deps = [ + "//pkg/abi/linux", + "//pkg/binary", + "//pkg/tcpip/header", + "//pkg/usermem", + "//test/packetimpact/testbench", + "@org_golang_x_sys//unix:go_default_library", + ], +) + validate_all_tests() [packetimpact_go_test( diff --git a/test/packetimpact/tests/ipv4_fragment_reassembly_test.go b/test/packetimpact/tests/ipv4_fragment_reassembly_test.go index d2203082d..ee050e2c6 100644 --- a/test/packetimpact/tests/ipv4_fragment_reassembly_test.go +++ b/test/packetimpact/tests/ipv4_fragment_reassembly_test.go @@ -45,8 +45,6 @@ func TestIPv4FragmentReassembly(t *testing.T) { ipPayloadLen int fragments []fragmentInfo expectReply bool - skip bool - skipReason string }{ { description: "basic reassembly", @@ -78,8 +76,6 @@ func TestIPv4FragmentReassembly(t *testing.T) { {offset: 2000, size: 1000, id: 7, more: 0}, }, expectReply: true, - skip: true, - skipReason: "gvisor.dev/issues/4971", }, { description: "fragment subset", @@ -91,8 +87,6 @@ func TestIPv4FragmentReassembly(t *testing.T) { {offset: 2000, size: 1000, id: 8, more: 0}, }, expectReply: true, - skip: true, - skipReason: "gvisor.dev/issues/4971", }, { description: "fragment overlap", @@ -104,16 +98,10 @@ func TestIPv4FragmentReassembly(t *testing.T) { {offset: 2000, size: 1000, id: 9, more: 0}, }, expectReply: false, - skip: true, - skipReason: "gvisor.dev/issues/4971", }, } for _, test := range tests { - if test.skip { - t.Skip("%s test skipped: %s", test.description, test.skipReason) - continue - } t.Run(test.description, func(t *testing.T) { dut := testbench.NewDUT(t) conn := dut.Net.NewIPv4Conn(t, testbench.IPv4{}, testbench.IPv4{}) diff --git a/test/packetimpact/tests/tcp_info_test.go b/test/packetimpact/tests/tcp_info_test.go new file mode 100644 index 000000000..69275e54b --- /dev/null +++ b/test/packetimpact/tests/tcp_info_test.go @@ -0,0 +1,109 @@ +// 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 tcp_info_test + +import ( + "flag" + "testing" + "time" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/binary" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/usermem" + "gvisor.dev/gvisor/test/packetimpact/testbench" +) + +func init() { + testbench.Initialize(flag.CommandLine) +} + +func TestTCPInfo(t *testing.T) { + // Create a socket, listen, TCP connect, and accept. + dut := testbench.NewDUT(t) + listenFD, remotePort := dut.CreateListener(t, unix.SOCK_STREAM, unix.IPPROTO_TCP, 1) + defer dut.Close(t, listenFD) + + conn := dut.Net.NewTCPIPv4(t, testbench.TCP{DstPort: &remotePort}, testbench.TCP{SrcPort: &remotePort}) + defer conn.Close(t) + conn.Connect(t) + + acceptFD, _ := dut.Accept(t, listenFD) + defer dut.Close(t, acceptFD) + + // Send and receive sample data. + sampleData := []byte("Sample Data") + samplePayload := &testbench.Payload{Bytes: sampleData} + dut.Send(t, acceptFD, sampleData, 0) + if _, err := conn.ExpectData(t, &testbench.TCP{}, samplePayload, time.Second); err != nil { + t.Fatalf("expected a packet with payload %v: %s", samplePayload, err) + } + conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck)}) + + info := linux.TCPInfo{} + infoBytes := dut.GetSockOpt(t, acceptFD, unix.SOL_TCP, unix.TCP_INFO, int32(linux.SizeOfTCPInfo)) + if got, want := len(infoBytes), linux.SizeOfTCPInfo; got != want { + t.Fatalf("expected %T, got %d bytes want %d bytes", info, got, want) + } + binary.Unmarshal(infoBytes, usermem.ByteOrder, &info) + + rtt := time.Duration(info.RTT) * time.Microsecond + rttvar := time.Duration(info.RTTVar) * time.Microsecond + rto := time.Duration(info.RTO) * time.Microsecond + if rtt == 0 || rttvar == 0 || rto == 0 { + t.Errorf("expected rtt(%v), rttvar(%v) and rto(%v) to be greater than zero", rtt, rttvar, rto) + } + if info.ReordSeen != 0 { + t.Errorf("expected the connection to not have any reordering, got: %v want: 0", info.ReordSeen) + } + if info.SndCwnd == 0 { + t.Errorf("expected send congestion window to be greater than zero") + } + if info.CaState != linux.TCP_CA_Open { + t.Errorf("expected the connection to be in open state, got: %v want: %v", info.CaState, linux.TCP_CA_Open) + } + + if t.Failed() { + t.FailNow() + } + + // Check the congestion control state and send congestion window after + // retransmission timeout. + seq := testbench.Uint32(uint32(*conn.RemoteSeqNum(t))) + dut.Send(t, acceptFD, sampleData, 0) + if _, err := conn.ExpectData(t, &testbench.TCP{}, samplePayload, time.Second); err != nil { + t.Fatalf("expected a packet with payload %v: %s", samplePayload, err) + } + + // Expect retransmission of the packet within 1.5*RTO. + timeout := time.Duration(float64(info.RTO)*1.5) * time.Microsecond + if _, err := conn.ExpectData(t, &testbench.TCP{SeqNum: seq}, samplePayload, timeout); err != nil { + t.Fatalf("expected a packet with payload %v: %s", samplePayload, err) + } + + info = linux.TCPInfo{} + infoBytes = dut.GetSockOpt(t, acceptFD, unix.SOL_TCP, unix.TCP_INFO, int32(linux.SizeOfTCPInfo)) + if got, want := len(infoBytes), linux.SizeOfTCPInfo; got != want { + t.Fatalf("expected %T, got %d bytes want %d bytes", info, got, want) + } + binary.Unmarshal(infoBytes, usermem.ByteOrder, &info) + if info.CaState != linux.TCP_CA_Loss { + t.Errorf("expected the connection to be in loss recovery, got: %v want: %v", info.CaState, linux.TCP_CA_Loss) + } + if info.SndCwnd != 1 { + t.Errorf("expected send congestion window to be 1, got: %v %v", info.SndCwnd) + } +} diff --git a/test/packetimpact/tests/tcp_noaccept_close_rst_test.go b/test/packetimpact/tests/tcp_noaccept_close_rst_test.go index f0af5352d..c874a8912 100644 --- a/test/packetimpact/tests/tcp_noaccept_close_rst_test.go +++ b/test/packetimpact/tests/tcp_noaccept_close_rst_test.go @@ -34,6 +34,21 @@ func TestTcpNoAcceptCloseReset(t *testing.T) { conn := dut.Net.NewTCPIPv4(t, testbench.TCP{DstPort: &remotePort}, testbench.TCP{SrcPort: &remotePort}) conn.Connect(t) defer conn.Close(t) + // We need to wait for POLLIN event on listenFd to know the connection is + // established. Otherwise there could be a race when we issue the Close + // command prior to the DUT receiving the last ack of the handshake and + // it will only respond RST instead of RST+ACK. + timeout := time.Second + pfds := dut.Poll(t, []unix.PollFd{{Fd: listenFd, Events: unix.POLLIN}}, timeout) + if n := len(pfds); n != 1 { + t.Fatalf("poll returned %d ready file descriptors, expected 1", n) + } + if readyFd := pfds[0].Fd; readyFd != listenFd { + t.Fatalf("poll returned an fd %d that was not requested (%d)", readyFd, listenFd) + } + if got, want := pfds[0].Revents, int16(unix.POLLIN); got&want == 0 { + t.Fatalf("poll returned no events in our interest, got: %#b, want: %#b", got, want) + } dut.Close(t, listenFd) if _, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagRst | header.TCPFlagAck)}, 1*time.Second); err != nil { t.Fatalf("expected a RST-ACK packet but got none: %s", err) diff --git a/test/packetimpact/tests/tcp_outside_the_window_test.go b/test/packetimpact/tests/tcp_outside_the_window_test.go index 1b041932a..8909a348e 100644 --- a/test/packetimpact/tests/tcp_outside_the_window_test.go +++ b/test/packetimpact/tests/tcp_outside_the_window_test.go @@ -84,6 +84,24 @@ func TestTCPOutsideTheWindow(t *testing.T) { if tt.expectACK && err != nil { t.Fatalf("expected an ACK packet within %s but got none: %s", timeout, err) } + // Data packets w/o SYN bits are always acked by Linux. Netstack ACK's data packets + // always right now. So only send a second segment and test for no ACK for packets + // with no data. + if tt.expectACK && tt.payload == nil { + // Sending another out-of-window segment immediately should not trigger + // an ACK if less than 500ms(default rate limit for out-of-window ACKs) + // has passed since the last ACK was sent. + t.Logf("sending another segment") + conn.Send(t, testbench.TCP{ + Flags: testbench.Uint8(tt.tcpFlags), + SeqNum: testbench.Uint32(uint32(conn.LocalSeqNum(t).Add(windowSize))), + }, tt.payload...) + timeout := 3 * time.Second + gotACK, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: localSeqNum}, timeout) + if err == nil { + t.Fatalf("expected no ACK packet but got one: %s", gotACK) + } + } if !tt.expectACK && gotACK != nil { t.Fatalf("expected no ACK packet within %s but got one: %s", timeout, gotACK) } diff --git a/test/packetimpact/tests/tcp_rack_test.go b/test/packetimpact/tests/tcp_rack_test.go index 0a2381c97..fb2a4cc90 100644 --- a/test/packetimpact/tests/tcp_rack_test.go +++ b/test/packetimpact/tests/tcp_rack_test.go @@ -70,8 +70,11 @@ func closeSACKConnection(t *testing.T, dut testbench.DUT, conn testbench.TCPIPv4 func getRTTAndRTO(t *testing.T, dut testbench.DUT, acceptFd int32) (rtt, rto time.Duration) { info := linux.TCPInfo{} - ret := dut.GetSockOpt(t, acceptFd, unix.SOL_TCP, unix.TCP_INFO, int32(linux.SizeOfTCPInfo)) - binary.Unmarshal(ret, usermem.ByteOrder, &info) + infoBytes := dut.GetSockOpt(t, acceptFd, unix.SOL_TCP, unix.TCP_INFO, int32(linux.SizeOfTCPInfo)) + if got, want := len(infoBytes), linux.SizeOfTCPInfo; got != want { + t.Fatalf("expected %T, got %d bytes want %d bytes", info, got, want) + } + binary.Unmarshal(infoBytes, usermem.ByteOrder, &info) return time.Duration(info.RTT) * time.Microsecond, time.Duration(info.RTO) * time.Microsecond } @@ -219,3 +222,200 @@ func TestRACKTLPWithSACK(t *testing.T) { } closeSACKConnection(t, dut, conn, acceptFd, listenFd) } + +// TestRACKWithoutReorder tests that without reordering RACK will retransmit the +// lost packets after reorder timer expires. +func TestRACKWithoutReorder(t *testing.T) { + dut, conn, acceptFd, listenFd := createSACKConnection(t) + seqNum1 := *conn.RemoteSeqNum(t) + + // Send ACK for data packets to establish RTT. + sendAndReceive(t, dut, conn, numPktsForRTT, acceptFd, true /* sendACK */) + seqNum1.UpdateForward(seqnum.Size(numPktsForRTT * payloadSize)) + + // We are not sending ACK for these packets. + const numPkts = 4 + sendAndReceive(t, dut, conn, numPkts, acceptFd, false /* sendACK */) + + // SACK for [3,4] packets. + sackBlock := make([]byte, 40) + start := seqNum1.Add(seqnum.Size(2 * payloadSize)) + end := start.Add(seqnum.Size(2 * payloadSize)) + sbOff := 0 + sbOff += header.EncodeNOP(sackBlock[sbOff:]) + sbOff += header.EncodeNOP(sackBlock[sbOff:]) + sbOff += header.EncodeSACKBlocks([]header.SACKBlock{{ + start, end, + }}, sackBlock[sbOff:]) + time.Sleep(simulatedRTT) + conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(seqNum1)), Options: sackBlock[:sbOff]}) + + // RACK marks #1 and #2 packets as lost and retransmits both after + // RTT + reorderWindow. The reorderWindow initially will be a small + // fraction of RTT. + rtt, _ := getRTTAndRTO(t, dut, acceptFd) + timeout := 2 * rtt + for i, sn := 0, seqNum1; i < 2; i++ { + if _, err := conn.Expect(t, testbench.TCP{SeqNum: testbench.Uint32(uint32(sn))}, timeout); err != nil { + t.Fatalf("expected payload was not received: %s", err) + } + sn.UpdateForward(seqnum.Size(payloadSize)) + } + closeSACKConnection(t, dut, conn, acceptFd, listenFd) +} + +// TestRACKWithReorder tests that RACK will retransmit segments when there is +// reordering in the connection and reorder timer expires. +func TestRACKWithReorder(t *testing.T) { + dut, conn, acceptFd, listenFd := createSACKConnection(t) + seqNum1 := *conn.RemoteSeqNum(t) + + // Send ACK for data packets to establish RTT. + sendAndReceive(t, dut, conn, numPktsForRTT, acceptFd, true /* sendACK */) + seqNum1.UpdateForward(seqnum.Size(numPktsForRTT * payloadSize)) + + // We are not sending ACK for these packets. + const numPkts = 4 + sendAndReceive(t, dut, conn, numPkts, acceptFd, false /* sendACK */) + + time.Sleep(simulatedRTT) + // SACK in reverse order for the connection to detect reorder. + var start seqnum.Value + var end seqnum.Value + for i := 0; i < numPkts-1; i++ { + sackBlock := make([]byte, 40) + sbOff := 0 + start = seqNum1.Add(seqnum.Size((numPkts - i - 1) * payloadSize)) + end = start.Add(seqnum.Size((i + 1) * payloadSize)) + sackBlock = make([]byte, 40) + sbOff = 0 + sbOff += header.EncodeNOP(sackBlock[sbOff:]) + sbOff += header.EncodeNOP(sackBlock[sbOff:]) + sbOff += header.EncodeSACKBlocks([]header.SACKBlock{{ + start, end, + }}, sackBlock[sbOff:]) + conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(seqNum1)), Options: sackBlock[:sbOff]}) + } + + // Send a DSACK block indicating both original and retransmitted + // packets are received, RACK will increase the reordering window on + // every DSACK. + dsackBlock := make([]byte, 40) + dbOff := 0 + start = seqNum1 + end = start.Add(seqnum.Size(2 * payloadSize)) + dbOff += header.EncodeNOP(dsackBlock[dbOff:]) + dbOff += header.EncodeNOP(dsackBlock[dbOff:]) + dbOff += header.EncodeSACKBlocks([]header.SACKBlock{{ + start, end, + }}, dsackBlock[dbOff:]) + conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(seqNum1 + numPkts*payloadSize)), Options: dsackBlock[:dbOff]}) + + seqNum1.UpdateForward(seqnum.Size(numPkts * payloadSize)) + sendTime := time.Now() + sendAndReceive(t, dut, conn, numPkts, acceptFd, false /* sendACK */) + + time.Sleep(simulatedRTT) + // Send SACK for [2-5] packets. + sackBlock := make([]byte, 40) + sbOff := 0 + start = seqNum1.Add(seqnum.Size(payloadSize)) + end = start.Add(seqnum.Size(3 * payloadSize)) + sbOff += header.EncodeNOP(sackBlock[sbOff:]) + sbOff += header.EncodeNOP(sackBlock[sbOff:]) + sbOff += header.EncodeSACKBlocks([]header.SACKBlock{{ + start, end, + }}, sackBlock[sbOff:]) + conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(seqNum1)), Options: sackBlock[:sbOff]}) + + // Expect the retransmission of #1 packet after RTT+ReorderWindow. + if _, err := conn.Expect(t, testbench.TCP{SeqNum: testbench.Uint32(uint32(seqNum1))}, time.Second); err != nil { + t.Fatalf("expected payload was not received: %s", err) + } + rtt, _ := getRTTAndRTO(t, dut, acceptFd) + diff := time.Now().Sub(sendTime) + if diff < rtt { + t.Fatalf("expected payload was received too sonn, within RTT") + } + + closeSACKConnection(t, dut, conn, acceptFd, listenFd) +} + +// TestRACKWithLostRetransmission tests that RACK will not enter RTO when a +// retransmitted segment is lost and enters fast recovery. +func TestRACKWithLostRetransmission(t *testing.T) { + dut, conn, acceptFd, listenFd := createSACKConnection(t) + seqNum1 := *conn.RemoteSeqNum(t) + + // Send ACK for data packets to establish RTT. + sendAndReceive(t, dut, conn, numPktsForRTT, acceptFd, true /* sendACK */) + seqNum1.UpdateForward(seqnum.Size(numPktsForRTT * payloadSize)) + + // We are not sending ACK for these packets. + const numPkts = 5 + sendAndReceive(t, dut, conn, numPkts, acceptFd, false /* sendACK */) + + // SACK for [2-5] packets. + sackBlock := make([]byte, 40) + start := seqNum1.Add(seqnum.Size(payloadSize)) + end := start.Add(seqnum.Size(4 * payloadSize)) + sbOff := 0 + sbOff += header.EncodeNOP(sackBlock[sbOff:]) + sbOff += header.EncodeNOP(sackBlock[sbOff:]) + sbOff += header.EncodeSACKBlocks([]header.SACKBlock{{ + start, end, + }}, sackBlock[sbOff:]) + time.Sleep(simulatedRTT) + conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(seqNum1)), Options: sackBlock[:sbOff]}) + + // RACK marks #1 packet as lost and retransmits it after + // RTT + reorderWindow. The reorderWindow is bounded between a small + // fraction of RTT and 1 RTT. + rtt, _ := getRTTAndRTO(t, dut, acceptFd) + timeout := 2 * rtt + if _, err := conn.Expect(t, testbench.TCP{SeqNum: testbench.Uint32(uint32(seqNum1))}, timeout); err != nil { + t.Fatalf("expected payload was not received: %s", err) + } + + // Send #6 packet. + payload := make([]byte, payloadSize) + dut.Send(t, acceptFd, payload, 0) + gotOne, err := conn.Expect(t, testbench.TCP{SeqNum: testbench.Uint32(uint32(seqNum1 + 5*payloadSize))}, time.Second) + if err != nil { + t.Fatalf("Expect #6: %s", err) + } + if gotOne == nil { + t.Fatalf("#6: expected a packet within a second but got none") + } + + // SACK for [2-6] packets. + sackBlock1 := make([]byte, 40) + start = seqNum1.Add(seqnum.Size(payloadSize)) + end = start.Add(seqnum.Size(5 * payloadSize)) + sbOff1 := 0 + sbOff1 += header.EncodeNOP(sackBlock1[sbOff1:]) + sbOff1 += header.EncodeNOP(sackBlock1[sbOff1:]) + sbOff1 += header.EncodeSACKBlocks([]header.SACKBlock{{ + start, end, + }}, sackBlock1[sbOff1:]) + time.Sleep(simulatedRTT) + conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(seqNum1)), Options: sackBlock1[:sbOff1]}) + + // Expect re-retransmission of #1 packet without entering an RTO. + if _, err := conn.Expect(t, testbench.TCP{SeqNum: testbench.Uint32(uint32(seqNum1))}, timeout); err != nil { + t.Fatalf("expected payload was not received: %s", err) + } + + // Check the congestion control state. + info := linux.TCPInfo{} + infoBytes := dut.GetSockOpt(t, acceptFd, unix.SOL_TCP, unix.TCP_INFO, int32(linux.SizeOfTCPInfo)) + if got, want := len(infoBytes), linux.SizeOfTCPInfo; got != want { + t.Fatalf("expected %T, got %d bytes want %d bytes", info, got, want) + } + binary.Unmarshal(infoBytes, usermem.ByteOrder, &info) + if info.CaState != linux.TCP_CA_Recovery { + t.Fatalf("expected connection to be in fast recovery, want: %v got: %v", linux.TCP_CA_Recovery, info.CaState) + } + + closeSACKConnection(t, dut, conn, acceptFd, listenFd) +} diff --git a/test/packetimpact/tests/tcp_zero_window_probe_retransmit_test.go b/test/packetimpact/tests/tcp_zero_window_probe_retransmit_test.go index 1ab9ee1b2..b15b8fc25 100644 --- a/test/packetimpact/tests/tcp_zero_window_probe_retransmit_test.go +++ b/test/packetimpact/tests/tcp_zero_window_probe_retransmit_test.go @@ -66,33 +66,39 @@ func TestZeroWindowProbeRetransmit(t *testing.T) { probeSeq := testbench.Uint32(uint32(*conn.RemoteSeqNum(t) - 1)) ackProbe := testbench.Uint32(uint32(*conn.RemoteSeqNum(t))) - startProbeDuration := time.Second - current := startProbeDuration - first := time.Now() // Ask the dut to send out data. dut.Send(t, acceptFd, sampleData, 0) + + var prev time.Duration // Expect the dut to keep the connection alive as long as the remote is // acknowledging the zero-window probes. - for i := 0; i < 5; i++ { + for i := 1; i <= 5; i++ { start := time.Now() // Expect zero-window probe with a timeout which is a function of the typical // first retransmission time. The retransmission times is supposed to // exponentially increase. - if _, err := conn.ExpectData(t, &testbench.TCP{SeqNum: probeSeq}, nil, 2*current); err != nil { + if _, err := conn.ExpectData(t, &testbench.TCP{SeqNum: probeSeq}, nil, time.Duration(i)*time.Second); err != nil { t.Fatalf("expected a probe with sequence number %d: loop %d", probeSeq, i) } - if i == 0 { - startProbeDuration = time.Now().Sub(first) - current = 2 * startProbeDuration + if i == 1 { + // Skip the first probe as computing transmit time for that is + // non-deterministic because of the arbitrary time taken for + // the dut to receive a send command and issue a send. continue } - // Check if the probes came at exponentially increasing intervals. - if got, want := time.Since(start), current-startProbeDuration; got < want { + + // Check if the time taken to receive the probe from the dut is + // increasing exponentially. To avoid flakes, use a correction + // factor for the expected duration which accounts for any + // scheduling non-determinism. + const timeCorrection = 200 * time.Millisecond + got := time.Since(start) + if want := (2 * prev) - timeCorrection; prev != 0 && got < want { t.Errorf("got zero probe %d after %s, want >= %s", i, got, want) } + prev = got // Acknowledge the zero-window probes from the dut. conn.Send(t, testbench.TCP{AckNum: ackProbe, Flags: testbench.Uint8(header.TCPFlagAck), WindowSize: testbench.Uint16(0)}) - current *= 2 } // Advertize non-zero window. conn.Send(t, testbench.TCP{AckNum: ackProbe, Flags: testbench.Uint8(header.TCPFlagAck)}) diff --git a/test/packetimpact/tests/udp_recv_mcast_bcast_test.go b/test/packetimpact/tests/udp_recv_mcast_bcast_test.go deleted file mode 100644 index b29c07825..000000000 --- a/test/packetimpact/tests/udp_recv_mcast_bcast_test.go +++ /dev/null @@ -1,115 +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_recv_mcast_bcast_test - -import ( - "context" - "flag" - "fmt" - "net" - "syscall" - "testing" - - "github.com/google/go-cmp/cmp" - "golang.org/x/sys/unix" - "gvisor.dev/gvisor/pkg/tcpip" - "gvisor.dev/gvisor/test/packetimpact/testbench" -) - -func init() { - testbench.Initialize(flag.CommandLine) -} - -func TestUDPRecvMcastBcast(t *testing.T) { - dut := testbench.NewDUT(t) - subnetBcastAddr := broadcastAddr(dut.Net.RemoteIPv4, net.CIDRMask(dut.Net.IPv4PrefixLength, 32)) - for _, v := range []struct { - bound, to net.IP - }{ - {bound: net.IPv4zero, to: subnetBcastAddr}, - {bound: net.IPv4zero, to: net.IPv4bcast}, - {bound: net.IPv4zero, to: net.IPv4allsys}, - - {bound: subnetBcastAddr, to: subnetBcastAddr}, - - // FIXME(gvisor.dev/issue/4896): Previously by the time subnetBcastAddr is - // created, IPv4PrefixLength is still 0 because genPseudoFlags is not called - // yet, it was only called in NewDUT, so the test didn't do what the author - // original intended to and becomes failing because we process all flags at - // the very beginning. - // - // {bound: subnetBcastAddr, to: net.IPv4bcast}, - - {bound: net.IPv4bcast, to: net.IPv4bcast}, - {bound: net.IPv4allsys, to: net.IPv4allsys}, - } { - t.Run(fmt.Sprintf("bound=%s,to=%s", v.bound, v.to), func(t *testing.T) { - boundFD, remotePort := dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, v.bound) - defer dut.Close(t, boundFD) - conn := dut.Net.NewUDPIPv4(t, testbench.UDP{DstPort: &remotePort}, testbench.UDP{SrcPort: &remotePort}) - defer conn.Close(t) - - payload := testbench.GenerateRandomPayload(t, 1<<10 /* 1 KiB */) - conn.SendIP( - t, - testbench.IPv4{DstAddr: testbench.Address(tcpip.Address(v.to.To4()))}, - testbench.UDP{}, - &testbench.Payload{Bytes: payload}, - ) - got, want := dut.Recv(t, boundFD, 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) - } - }) - } -} - -func TestUDPDoesntRecvMcastBcastOnUnicastAddr(t *testing.T) { - dut := testbench.NewDUT(t) - boundFD, remotePort := dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, dut.Net.RemoteIPv4) - dut.SetSockOptTimeval(t, boundFD, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 1, Usec: 0}) - defer dut.Close(t, boundFD) - conn := dut.Net.NewUDPIPv4(t, testbench.UDP{DstPort: &remotePort}, testbench.UDP{SrcPort: &remotePort}) - defer conn.Close(t) - - for _, to := range []net.IP{ - broadcastAddr(dut.Net.RemoteIPv4, net.CIDRMask(dut.Net.IPv4PrefixLength, 32)), - net.IPv4(255, 255, 255, 255), - net.IPv4(224, 0, 0, 1), - } { - t.Run(fmt.Sprint("to=%s", to), func(t *testing.T) { - payload := testbench.GenerateRandomPayload(t, 1<<10 /* 1 KiB */) - conn.SendIP( - t, - testbench.IPv4{DstAddr: testbench.Address(tcpip.Address(to.To4()))}, - testbench.UDP{}, - &testbench.Payload{Bytes: payload}, - ) - ret, payload, errno := dut.RecvWithErrno(context.Background(), t, boundFD, 100, 0) - if errno != syscall.EAGAIN || errno != syscall.EWOULDBLOCK { - t.Errorf("Recv got unexpected result, ret=%d, payload=%q, errno=%s", ret, payload, errno) - } - }) - } -} - -func broadcastAddr(ip net.IP, mask net.IPMask) net.IP { - result := make(net.IP, net.IPv4len) - ip4 := ip.To4() - for i := range ip4 { - result[i] = ip4[i] | ^mask[i] - } - return result -} diff --git a/test/packetimpact/tests/udp_send_recv_dgram_test.go b/test/packetimpact/tests/udp_send_recv_dgram_test.go index 7ee2c8014..6e45cb143 100644 --- a/test/packetimpact/tests/udp_send_recv_dgram_test.go +++ b/test/packetimpact/tests/udp_send_recv_dgram_test.go @@ -15,13 +15,18 @@ package udp_send_recv_dgram_test import ( + "context" "flag" + "fmt" "net" + "syscall" "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" ) @@ -30,74 +35,295 @@ func init() { } type udpConn interface { - Send(*testing.T, testbench.UDP, ...testbench.Layer) - ExpectData(*testing.T, testbench.UDP, testbench.Payload, time.Duration) (testbench.Layers, error) - Drain(*testing.T) + 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()) + }() - for _, isIPv4 := range []bool{true, false} { - ipVersionName := "IPv6" - if isIPv4 { - ipVersionName = "IPv4" - } - t.Run(ipVersionName, func(t *testing.T) { - var addr net.IP - if isIPv4 { - addr = dut.Net.RemoteIPv4 - } else { - addr = dut.Net.RemoteIPv6 + 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, + }, + ) + } } - boundFD, remotePort := dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, addr) - defer dut.Close(t, boundFD) - - var conn udpConn - var localAddr unix.Sockaddr - if isIPv4 { - v4Conn := dut.Net.NewUDPIPv4(t, testbench.UDP{DstPort: &remotePort}, testbench.UDP{SrcPort: &remotePort}) - localAddr = v4Conn.LocalAddr(t) - conn = &v4Conn - } else { - v6Conn := dut.Net.NewUDPIPv6(t, testbench.UDP{DstPort: &remotePort}, testbench.UDP{SrcPort: &remotePort}) - localAddr = v6Conn.LocalAddr(t, dut.Net.RemoteDevID) - conn = &v6Conn - } - defer conn.Close(t) - - testCases := []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. + } + for _, tc := range testCases { + boundTestCaseName := "unbound" + if tc.bindTo != nil { + boundTestCaseName = fmt.Sprintf("bindTo=%s", tc.bindTo) } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Run("Send", func(t *testing.T) { - conn.Send(t, testbench.UDP{}, &testbench.Payload{Bytes: tc.payload}) - got, want := dut.Recv(t, boundFD, int32(len(tc.payload)+1), 0), tc.payload - if diff := cmp.Diff(want, got); diff != "" { - t.Fatalf("received payload does not match sent payload, diff (-want, +got):\n%s", diff) + 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 } - }) - t.Run("Recv", func(t *testing.T) { - conn.Drain(t) - if got, want := int(dut.SendTo(t, boundFD, tc.payload, 0, localAddr)), len(tc.payload); got != want { - t.Fatalf("short write got: %d, want: %d", got, want) + 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) } - if _, err := conn.ExpectData(t, testbench.UDP{SrcPort: &remotePort}, testbench.Payload{Bytes: tc.payload}, time.Second); err != nil { + 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 != syscall.EAGAIN || errno != syscall.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) } } |