gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/packetimpact/tests/udp_icmp_error_propagation_test.go (about)

     1  // Copyright 2020 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package udp_icmp_error_propagation_test
    16  
    17  import (
    18  	"context"
    19  	"flag"
    20  	"fmt"
    21  	"net"
    22  	"sync"
    23  	"testing"
    24  	"time"
    25  
    26  	"golang.org/x/sys/unix"
    27  	"gvisor.dev/gvisor/pkg/tcpip/header"
    28  	"gvisor.dev/gvisor/test/packetimpact/testbench"
    29  )
    30  
    31  func init() {
    32  	testbench.Initialize(flag.CommandLine)
    33  }
    34  
    35  type connectionMode bool
    36  
    37  func (c connectionMode) String() string {
    38  	if c {
    39  		return "Connected"
    40  	}
    41  	return "Connectionless"
    42  }
    43  
    44  type icmpError int
    45  
    46  const (
    47  	portUnreachable icmpError = iota
    48  	timeToLiveExceeded
    49  )
    50  
    51  func (e icmpError) String() string {
    52  	switch e {
    53  	case portUnreachable:
    54  		return "PortUnreachable"
    55  	case timeToLiveExceeded:
    56  		return "TimeToLiveExpired"
    57  	}
    58  	return "Unknown ICMP error"
    59  }
    60  
    61  func (e icmpError) ToICMPv4(payload []byte) *testbench.ICMPv4 {
    62  	switch e {
    63  	case portUnreachable:
    64  		return &testbench.ICMPv4{
    65  			Type:    testbench.ICMPv4Type(header.ICMPv4DstUnreachable),
    66  			Code:    testbench.ICMPv4Code(header.ICMPv4PortUnreachable),
    67  			Payload: payload,
    68  		}
    69  	case timeToLiveExceeded:
    70  		return &testbench.ICMPv4{
    71  			Type:    testbench.ICMPv4Type(header.ICMPv4TimeExceeded),
    72  			Code:    testbench.ICMPv4Code(header.ICMPv4TTLExceeded),
    73  			Payload: payload,
    74  		}
    75  	}
    76  	return nil
    77  }
    78  
    79  type errorDetection struct {
    80  	name         string
    81  	useValidConn bool
    82  	f            func(context.Context, *testing.T, testData)
    83  }
    84  
    85  type testData struct {
    86  	dut        *testbench.DUT
    87  	conn       *testbench.UDPIPv4
    88  	remoteFD   int32
    89  	remotePort uint16
    90  	cleanFD    int32
    91  	cleanPort  uint16
    92  	wantErrno  unix.Errno
    93  }
    94  
    95  // wantErrno computes the errno to expect given the connection mode of a UDP
    96  // socket and the ICMP error it will receive.
    97  func wantErrno(c connectionMode, icmpErr icmpError) unix.Errno {
    98  	if c && icmpErr == portUnreachable {
    99  		return unix.ECONNREFUSED
   100  	}
   101  	return unix.Errno(0)
   102  }
   103  
   104  // sendICMPError sends an ICMP error message in response to a UDP datagram.
   105  func sendICMPError(t *testing.T, conn *testbench.UDPIPv4, icmpErr icmpError, udp *testbench.UDP) {
   106  	t.Helper()
   107  
   108  	ip, ok := udp.Prev().(*testbench.IPv4)
   109  	if !ok {
   110  		t.Fatalf("expected %s to be IPv4", udp.Prev())
   111  	}
   112  	if icmpErr == timeToLiveExceeded {
   113  		*ip.TTL = 1
   114  		// Let serialization recalculate the checksum since we set the TTL
   115  		// to 1.
   116  		ip.Checksum = nil
   117  	}
   118  
   119  	icmpPayload := testbench.Layers{ip, udp}
   120  	bytes, err := icmpPayload.ToBytes()
   121  	if err != nil {
   122  		t.Fatalf("got icmpPayload.ToBytes() = (_, %s), want = (_, nil)", err)
   123  	}
   124  
   125  	layers := conn.CreateFrame(t, nil)
   126  	layers[len(layers)-1] = icmpErr.ToICMPv4(bytes)
   127  	conn.SendFrameStateless(t, layers)
   128  }
   129  
   130  // testRecv tests observing the ICMP error through the recv unix. A packet
   131  // is sent to the DUT, and if wantErrno is non-zero, then the first recv should
   132  // fail and the second should succeed. Otherwise if wantErrno is zero then the
   133  // first recv should succeed immediately.
   134  func testRecv(ctx context.Context, t *testing.T, d testData) {
   135  	t.Helper()
   136  
   137  	// Check that receiving on the clean socket works.
   138  	d.conn.Send(t, testbench.UDP{DstPort: &d.cleanPort})
   139  	d.dut.Recv(t, d.cleanFD, 100, 0)
   140  
   141  	d.conn.Send(t, testbench.UDP{})
   142  
   143  	if d.wantErrno != unix.Errno(0) {
   144  		ret, _, err := d.dut.RecvWithErrno(ctx, t, d.remoteFD, 100, 0)
   145  		if ret != -1 {
   146  			t.Fatalf("recv after ICMP error succeeded unexpectedly, expected (%[1]d) %[1]v", d.wantErrno)
   147  		}
   148  		if err != d.wantErrno {
   149  			t.Fatalf("recv after ICMP error resulted in error (%[1]d) %[1]v, expected (%[2]d) %[2]v", err, d.wantErrno)
   150  		}
   151  	}
   152  
   153  	d.dut.Recv(t, d.remoteFD, 100, 0)
   154  }
   155  
   156  // testSendTo tests observing the ICMP error through the send syscall. If
   157  // wantErrno is non-zero, the first send should fail and a subsequent send
   158  // should succeed; while if wantErrno is zero then the first send should just
   159  // succeed.
   160  func testSendTo(ctx context.Context, t *testing.T, d testData) {
   161  	// Check that sending on the clean socket works.
   162  	d.dut.SendTo(t, d.cleanFD, nil, 0, d.conn.LocalAddr(t))
   163  	if _, err := d.conn.Expect(t, testbench.UDP{SrcPort: &d.cleanPort}, time.Second); err != nil {
   164  		t.Fatalf("did not receive UDP packet from clean socket on DUT: %s", err)
   165  	}
   166  
   167  	if d.wantErrno != unix.Errno(0) {
   168  		ret, err := d.dut.SendToWithErrno(ctx, t, d.remoteFD, nil, 0, d.conn.LocalAddr(t))
   169  
   170  		if ret != -1 {
   171  			t.Fatalf("sendto after ICMP error succeeded unexpectedly, expected (%[1]d) %[1]v", d.wantErrno)
   172  		}
   173  		if err != d.wantErrno {
   174  			t.Fatalf("sendto after ICMP error resulted in error (%[1]d) %[1]v, expected (%[2]d) %[2]v", err, d.wantErrno)
   175  		}
   176  	}
   177  
   178  	d.dut.SendTo(t, d.remoteFD, nil, 0, d.conn.LocalAddr(t))
   179  	if _, err := d.conn.Expect(t, testbench.UDP{}, time.Second); err != nil {
   180  		t.Fatalf("did not receive UDP packet as expected: %s", err)
   181  	}
   182  }
   183  
   184  func testSockOpt(_ context.Context, t *testing.T, d testData) {
   185  	// Check that there's no pending error on the clean socket.
   186  	if errno := unix.Errno(d.dut.GetSockOptInt(t, d.cleanFD, unix.SOL_SOCKET, unix.SO_ERROR)); errno != unix.Errno(0) {
   187  		t.Fatalf("unexpected error (%[1]d) %[1]v on clean socket", errno)
   188  	}
   189  
   190  	if errno := unix.Errno(d.dut.GetSockOptInt(t, d.remoteFD, unix.SOL_SOCKET, unix.SO_ERROR)); errno != d.wantErrno {
   191  		t.Fatalf("SO_ERROR sockopt after ICMP error is (%[1]d) %[1]v, expected (%[2]d) %[2]v", errno, d.wantErrno)
   192  	}
   193  
   194  	// Check that after clearing socket error, sending doesn't fail.
   195  	d.dut.SendTo(t, d.remoteFD, nil, 0, d.conn.LocalAddr(t))
   196  	if _, err := d.conn.Expect(t, testbench.UDP{}, time.Second); err != nil {
   197  		t.Fatalf("did not receive UDP packet as expected: %s", err)
   198  	}
   199  }
   200  
   201  // TestUDPICMPErrorPropagation tests that ICMP error messages in response to
   202  // UDP datagrams are processed correctly. RFC 1122 section 4.1.3.3 states that:
   203  // "UDP MUST pass to the application layer all ICMP error messages that it
   204  // receives from the IP layer."
   205  //
   206  // The test cases are parametrized in 3 dimensions: 1. the UDP socket is either
   207  // put into connection mode or left connectionless, 2. the ICMP message type
   208  // and code, and 3. the method by which the ICMP error is observed on the
   209  // socket: sendto, recv, or getsockopt(SO_ERROR).
   210  //
   211  // Linux's udp(7) man page states: "All fatal errors will be passed to the user
   212  // as an error return even when the socket is not connected. This includes
   213  // asynchronous errors received from the network." In practice, the only
   214  // combination of parameters to the test that causes an error to be observable
   215  // on the UDP socket is receiving a port unreachable message on a connected
   216  // socket.
   217  func TestUDPICMPErrorPropagation(t *testing.T) {
   218  	for _, connect := range []connectionMode{true, false} {
   219  		for _, icmpErr := range []icmpError{portUnreachable, timeToLiveExceeded} {
   220  			wantErrno := wantErrno(connect, icmpErr)
   221  
   222  			for _, errDetect := range []errorDetection{
   223  				{"SendTo", false, testSendTo},
   224  				// Send to an address that's different from the one that caused an ICMP
   225  				// error to be returned.
   226  				{"SendToValid", true, testSendTo},
   227  				{"Recv", false, testRecv},
   228  				{"SockOpt", false, testSockOpt},
   229  			} {
   230  				t.Run(fmt.Sprintf("%s/%s/%s", connect, icmpErr, errDetect.name), func(t *testing.T) {
   231  					dut := testbench.NewDUT(t)
   232  
   233  					remoteFD, remotePort := dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, net.IPv4zero)
   234  					defer dut.Close(t, remoteFD)
   235  
   236  					// Create a second, clean socket on the DUT to ensure that the ICMP
   237  					// error messages only affect the sockets they are intended for.
   238  					cleanFD, cleanPort := dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, net.IPv4zero)
   239  					defer dut.Close(t, cleanFD)
   240  
   241  					conn := dut.Net.NewUDPIPv4(t, testbench.UDP{DstPort: &remotePort}, testbench.UDP{SrcPort: &remotePort})
   242  					defer conn.Close(t)
   243  
   244  					if connect {
   245  						dut.Connect(t, remoteFD, conn.LocalAddr(t))
   246  						dut.Connect(t, cleanFD, conn.LocalAddr(t))
   247  					}
   248  
   249  					dut.SendTo(t, remoteFD, nil, 0, conn.LocalAddr(t))
   250  					udp, err := conn.Expect(t, testbench.UDP{}, time.Second)
   251  					if err != nil {
   252  						t.Fatalf("did not receive message from DUT: %s", err)
   253  					}
   254  
   255  					sendICMPError(t, &conn, icmpErr, udp)
   256  
   257  					errDetectConn := &conn
   258  					if errDetect.useValidConn {
   259  						// connClean is a UDP socket on the test runner that was not
   260  						// involved in the generation of the ICMP error. As such,
   261  						// interactions between it and the DUT should be independent of
   262  						// the ICMP error at least at the port level.
   263  						connClean := dut.Net.NewUDPIPv4(t, testbench.UDP{DstPort: &remotePort}, testbench.UDP{SrcPort: &remotePort})
   264  						defer connClean.Close(t)
   265  
   266  						errDetectConn = &connClean
   267  					}
   268  
   269  					errDetect.f(context.Background(), t, testData{&dut, errDetectConn, remoteFD, remotePort, cleanFD, cleanPort, wantErrno})
   270  				})
   271  			}
   272  		}
   273  	}
   274  }
   275  
   276  // TestICMPErrorDuringUDPRecv tests behavior when a UDP socket is in the middle
   277  // of a blocking recv and receives an ICMP error.
   278  func TestICMPErrorDuringUDPRecv(t *testing.T) {
   279  	for _, connect := range []connectionMode{true, false} {
   280  		for _, icmpErr := range []icmpError{portUnreachable, timeToLiveExceeded} {
   281  			wantErrno := wantErrno(connect, icmpErr)
   282  
   283  			t.Run(fmt.Sprintf("%s/%s", connect, icmpErr), func(t *testing.T) {
   284  				dut := testbench.NewDUT(t)
   285  
   286  				remoteFD, remotePort := dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, net.IPv4zero)
   287  				defer dut.Close(t, remoteFD)
   288  
   289  				// Create a second, clean socket on the DUT to ensure that the ICMP
   290  				// error messages only affect the sockets they are intended for.
   291  				cleanFD, cleanPort := dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, net.IPv4zero)
   292  				defer dut.Close(t, cleanFD)
   293  
   294  				conn := dut.Net.NewUDPIPv4(t, testbench.UDP{DstPort: &remotePort}, testbench.UDP{SrcPort: &remotePort})
   295  				defer conn.Close(t)
   296  
   297  				if connect {
   298  					dut.Connect(t, remoteFD, conn.LocalAddr(t))
   299  					dut.Connect(t, cleanFD, conn.LocalAddr(t))
   300  				}
   301  
   302  				dut.SendTo(t, remoteFD, nil, 0, conn.LocalAddr(t))
   303  				udp, err := conn.Expect(t, testbench.UDP{}, time.Second)
   304  				if err != nil {
   305  					t.Fatalf("did not receive message from DUT: %s", err)
   306  				}
   307  
   308  				var wg sync.WaitGroup
   309  				wg.Add(2)
   310  				go func() {
   311  					defer wg.Done()
   312  
   313  					if wantErrno != unix.Errno(0) {
   314  						ret, _, err := dut.RecvWithErrno(context.Background(), t, remoteFD, 100, 0)
   315  						if ret != -1 {
   316  							t.Errorf("recv during ICMP error succeeded unexpectedly, expected (%[1]d) %[1]v", wantErrno)
   317  							return
   318  						}
   319  						if err != wantErrno {
   320  							t.Errorf("recv during ICMP error resulted in error (%[1]d) %[1]v, expected (%[2]d) %[2]v", err, wantErrno)
   321  							return
   322  						}
   323  					}
   324  
   325  					if ret, _, err := dut.RecvWithErrno(context.Background(), t, remoteFD, 100, 0); ret == -1 {
   326  						t.Errorf("recv after ICMP error failed with (%[1]d) %[1]", err)
   327  					}
   328  				}()
   329  
   330  				go func() {
   331  					defer wg.Done()
   332  
   333  					if ret, _, err := dut.RecvWithErrno(context.Background(), t, cleanFD, 100, 0); ret == -1 {
   334  						t.Errorf("recv on clean socket failed with (%[1]d) %[1]", err)
   335  					}
   336  				}()
   337  
   338  				// TODO(b/155684889) This sleep is to allow time for the DUT to
   339  				// actually call recv since we want the ICMP error to arrive during the
   340  				// blocking recv, and should be replaced when a better synchronization
   341  				// alternative is available.
   342  				time.Sleep(2 * time.Second)
   343  
   344  				sendICMPError(t, &conn, icmpErr, udp)
   345  
   346  				conn.Send(t, testbench.UDP{DstPort: &cleanPort})
   347  				conn.Send(t, testbench.UDP{})
   348  				wg.Wait()
   349  			})
   350  		}
   351  	}
   352  }