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 }