// 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 ipv4_test

import (
	"bytes"
	"encoding/hex"
	"fmt"
	"io"
	"math"
	"net"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"gvisor.dev/gvisor/pkg/buffer"
	"gvisor.dev/gvisor/pkg/refs"
	"gvisor.dev/gvisor/pkg/sync"
	"gvisor.dev/gvisor/pkg/tcpip"
	"gvisor.dev/gvisor/pkg/tcpip/checker"
	"gvisor.dev/gvisor/pkg/tcpip/checksum"
	"gvisor.dev/gvisor/pkg/tcpip/faketime"
	"gvisor.dev/gvisor/pkg/tcpip/header"
	"gvisor.dev/gvisor/pkg/tcpip/link/channel"
	"gvisor.dev/gvisor/pkg/tcpip/link/sniffer"
	"gvisor.dev/gvisor/pkg/tcpip/network/arp"
	iptestutil "gvisor.dev/gvisor/pkg/tcpip/network/internal/testutil"
	"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
	"gvisor.dev/gvisor/pkg/tcpip/prependable"
	"gvisor.dev/gvisor/pkg/tcpip/stack"
	"gvisor.dev/gvisor/pkg/tcpip/testutil"
	"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
	"gvisor.dev/gvisor/pkg/tcpip/transport/raw"
	"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
	"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
	"gvisor.dev/gvisor/pkg/waiter"
)

const (
	extraHeaderReserve = 50
	defaultMTU         = 65536
)

type testContext struct {
	s     *stack.Stack
	clock *faketime.ManualClock
}

var _ stack.MulticastForwardingEventDispatcher = (*fakeMulticastEventDispatcher)(nil)

type fakeMulticastEventDispatcher struct{}

func (m *fakeMulticastEventDispatcher) OnMissingRoute(context stack.MulticastPacketContext) {}

func (m *fakeMulticastEventDispatcher) OnUnexpectedInputInterface(context stack.MulticastPacketContext, expectedInputInterface tcpip.NICID) {
}

func newTestContext() testContext {
	clock := faketime.NewManualClock()
	s := stack.New(stack.Options{
		NetworkProtocols:   []stack.NetworkProtocolFactory{arp.NewProtocol, ipv4.NewProtocol},
		TransportProtocols: []stack.TransportProtocolFactory{udp.NewProtocol},
		Clock:              clock,
		RawFactory:         raw.EndpointFactory{},
	})
	return testContext{s: s, clock: clock}
}

func (ctx testContext) cleanup() {
	ctx.s.Close()
	ctx.s.Wait()
	refs.DoRepeatedLeakCheck()
}

func TestExcludeBroadcast(t *testing.T) {
	ctx := newTestContext()
	defer ctx.cleanup()
	s := ctx.s

	ch := channel.New(256, defaultMTU, "")
	defer ch.Close()
	ep := stack.LinkEndpoint(ch)
	if testing.Verbose() {
		ep = sniffer.New(ep)
	}
	if err := s.CreateNIC(1, ep); err != nil {
		t.Fatalf("CreateNIC failed: %v", err)
	}

	s.SetRouteTable([]tcpip.Route{{
		Destination: header.IPv4EmptySubnet,
		NIC:         1,
	}})

	randomAddr := tcpip.FullAddress{NIC: 1, Addr: tcpip.AddrFromSlice([]byte("\x0a\x00\x00\x01")), Port: 53}

	var wq waiter.Queue
	t.Run("WithoutPrimaryAddress", func(t *testing.T) {
		ep, err := s.NewEndpoint(udp.ProtocolNumber, ipv4.ProtocolNumber, &wq)
		if err != nil {
			t.Fatal(err)
		}
		defer ep.Close()

		// Cannot connect using a broadcast address as the source.
		{
			err := ep.Connect(randomAddr)
			if _, ok := err.(*tcpip.ErrNetworkUnreachable); !ok {
				t.Errorf("got ep.Connect(...) = %v, want = %v", err, &tcpip.ErrHostUnreachable{})
			}
		}

		// However, we can bind to a broadcast address to listen.
		if err := ep.Bind(tcpip.FullAddress{Addr: header.IPv4Broadcast, Port: 53, NIC: 1}); err != nil {
			t.Errorf("Bind failed: %v", err)
		}
	})

	t.Run("WithPrimaryAddress", func(t *testing.T) {
		ep, err := s.NewEndpoint(udp.ProtocolNumber, ipv4.ProtocolNumber, &wq)
		if err != nil {
			t.Fatal(err)
		}
		defer ep.Close()

		// Add a valid primary endpoint address, now we can connect.
		protocolAddr := tcpip.ProtocolAddress{
			Protocol:          ipv4.ProtocolNumber,
			AddressWithPrefix: tcpip.AddrFromSlice([]byte("\x0a\x00\x00\x02")).WithPrefix(),
		}
		if err := s.AddProtocolAddress(1, protocolAddr, stack.AddressProperties{}); err != nil {
			t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", 1, protocolAddr, err)
		}
		if err := ep.Connect(randomAddr); err != nil {
			t.Errorf("Connect failed: %v", err)
		}
	})
}

const (
	incomingNICID = 1
	outgoingNICID = 2
)

var (
	incomingIPv4Addr = tcpip.AddressWithPrefix{
		Address:   testutil.MustParse4("10.0.0.1"),
		PrefixLen: 8,
	}
	outgoingIPv4Addr = tcpip.AddressWithPrefix{
		Address:   testutil.MustParse4("11.0.0.1"),
		PrefixLen: 8,
	}
	defaultEndpointConfigs = map[tcpip.NICID]tcpip.AddressWithPrefix{
		incomingNICID: incomingIPv4Addr,
		outgoingNICID: outgoingIPv4Addr,
	}
	multicastIPv4Addr = testutil.MustParse4("225.0.0.0")
	remoteIPv4Addr1   = testutil.MustParse4("10.0.0.2")
	remoteIPv4Addr2   = testutil.MustParse4("11.0.0.2")
)

func TestAddMulticastRouteIPv4Errors(t *testing.T) {
	incomingEpSubnet := incomingIPv4Addr.Subnet()
	wantErr := &tcpip.ErrBadAddress{}

	tests := []struct {
		name    string
		srcAddr tcpip.Address
	}{
		{
			name:    "subnet-local broadcast source",
			srcAddr: incomingEpSubnet.Broadcast().To4(),
		},
		{
			name:    "broadcast source",
			srcAddr: header.IPv4Broadcast,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()
			s := ctx.s

			for nicID, addr := range defaultEndpointConfigs {
				ep := channel.New(1, ipv4.MaxTotalSize, "")
				defer ep.Close()

				if err := s.CreateNIC(nicID, ep); err != nil {
					t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err)
				}
				addr := tcpip.ProtocolAddress{
					Protocol:          header.IPv4ProtocolNumber,
					AddressWithPrefix: addr,
				}
				if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil {
					t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, addr, err)
				}
			}

			if _, err := s.EnableMulticastForwardingForProtocol(ipv4.ProtocolNumber, &fakeMulticastEventDispatcher{}); err != nil {
				t.Fatalf("s.EnableMulticastForwardingForProtocol(%d, _): (_, %s)", ipv4.ProtocolNumber, err)
			}

			outgoingInterfaces := []stack.MulticastRouteOutgoingInterface{{ID: outgoingNICID, MinTTL: 1}}

			addresses := stack.UnicastSourceAndMulticastDestination{
				Source:      test.srcAddr,
				Destination: multicastIPv4Addr,
			}

			route := stack.MulticastRoute{
				ExpectedInputInterface: outgoingNICID,
				OutgoingInterfaces:     outgoingInterfaces,
			}

			err := s.AddMulticastRoute(ipv4.ProtocolNumber, addresses, route)

			if !cmp.Equal(err, wantErr, cmpopts.EquateErrors()) {
				t.Errorf("got s.AddMulticastRoute(%d, %#v, %#v) = %s, want %s", ipv4.ProtocolNumber, addresses, route, err, wantErr)
			}
		})
	}
}

type icmpError struct {
	icmpType header.ICMPv4Type
	icmpCode header.ICMPv4Code
}

type packetOptions struct {
	ipFlags       uint8
	payloadLength int
	options       header.IPv4Options
}

func newICMPEchoPacket(t *testing.T, srcAddr, dstAddr tcpip.Address, ttl uint8, options packetOptions) (*stack.PacketBuffer, []byte) {
	const (
		arbitraryICMPHeaderSequence = 123
		randomIdent                 = 42
	)

	t.Helper()
	ipHeaderLength := header.IPv4MinimumSize + len(options.options)
	if ipHeaderLength > header.IPv4MaximumHeaderSize {
		t.Fatalf("ipHeaderLength = %d, want <= %d ", ipHeaderLength, header.IPv4MaximumHeaderSize)
	}
	totalLength := ipHeaderLength + header.ICMPv4MinimumSize + options.payloadLength
	hdr := prependable.New(totalLength)
	hdr.Prepend(options.payloadLength)
	icmpH := header.ICMPv4(hdr.Prepend(header.ICMPv4MinimumSize))
	icmpH.SetIdent(randomIdent)
	icmpH.SetSequence(arbitraryICMPHeaderSequence)
	icmpH.SetType(header.ICMPv4Echo)
	icmpH.SetCode(header.ICMPv4UnusedCode)
	icmpH.SetChecksum(0)
	icmpH.SetChecksum(^checksum.Checksum(icmpH, 0))
	ip := header.IPv4(hdr.Prepend(ipHeaderLength))
	ip.Encode(&header.IPv4Fields{
		TotalLength: uint16(totalLength),
		Protocol:    uint8(header.ICMPv4ProtocolNumber),
		TTL:         ttl,
		SrcAddr:     srcAddr,
		DstAddr:     dstAddr,
		Flags:       options.ipFlags,
	})
	if len(options.options) != 0 {
		ip.SetHeaderLength(uint8(ipHeaderLength))
		// Copy options manually. We do not use Encode for options so we can
		// verify malformed options with handcrafted payloads.
		if want, got := copy(ip.Options(), options.options), len(options.options); want != got {
			t.Fatalf("got copy(ip.Options(), test.options) = %d, want = %d", got, want)
		}
	}
	ip.SetChecksum(0)
	ip.SetChecksum(^ip.CalculateChecksum())

	expectedICMPPayloadLength := func() int {
		maxICMPPacketLength := header.IPv4MinimumProcessableDatagramSize
		maxICMPPayloadLength := maxICMPPacketLength - header.ICMPv4MinimumSize - header.IPv4MinimumSize
		if len(hdr.View()) > maxICMPPayloadLength {
			return maxICMPPayloadLength
		}
		return len(hdr.View())
	}

	pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
		Payload: buffer.MakeWithData(hdr.View()),
	})
	pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber

	return pkt, hdr.View()[:expectedICMPPayloadLength()]
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func checkFragements(t *testing.T, ep *channel.Endpoint, expectedFragments []fragmentInfo, requestPkt *stack.PacketBuffer) {
	t.Helper()
	var fragmentedPackets []*stack.PacketBuffer
	for i := 0; i < len(expectedFragments); i++ {
		reply := ep.Read()
		if reply == nil {
			t.Fatal("Expected ICMP Echo fragment through outgoing NIC")
		}
		fragmentedPackets = append(fragmentedPackets, reply)
	}

	// The forwarded packet's TTL will have been decremented.
	ipHeader := header.IPv4(requestPkt.NetworkHeader().Slice())
	ipHeader.SetTTL(ipHeader.TTL() - 1)

	// Forwarded packets have available header bytes equalling the sum of the
	// maximum IP header size and the maximum size allocated for link layer
	// headers. In this case, no size is allocated for link layer headers.
	expectedAvailableHeaderBytes := header.IPv4MaximumHeaderSize
	if err := compareFragments(fragmentedPackets, requestPkt, defaultMTU, expectedFragments, header.ICMPv4ProtocolNumber, true /* withIPHeader */, expectedAvailableHeaderBytes); err != nil {
		t.Error(err)
	}
	for _, pkt := range fragmentedPackets {
		pkt.DecRef()
	}
}

func TestForwarding(t *testing.T) {
	const randomTimeOffset = 0x10203040

	unreachableIPv4Addr := testutil.MustParse4("12.0.0.2")

	tests := []struct {
		name                                 string
		TTL                                  uint8
		srcAddr                              tcpip.Address
		dstAddr                              tcpip.Address
		options                              header.IPv4Options
		forwardedOptions                     header.IPv4Options
		icmpError                            *icmpError
		closeOutgoingEndpoint                bool
		expectedPacketUnrouteableErrors      uint64
		expectedInitializingSourceErrors     uint64
		expectedLinkLocalSourceErrors        uint64
		expectedLinkLocalDestErrors          uint64
		expectedMalformedPacketErrors        uint64
		expectedExhaustedTTLErrors           uint64
		expectedOutgoingEndpointClosedErrors uint64
		expectPacketForwarded                bool
	}{
		{
			name:    "TTL of zero",
			TTL:     0,
			srcAddr: remoteIPv4Addr1,
			dstAddr: remoteIPv4Addr2,
			icmpError: &icmpError{
				icmpType: header.ICMPv4TimeExceeded,
				icmpCode: header.ICMPv4TTLExceeded,
			},
			expectedExhaustedTTLErrors: 1,
			expectPacketForwarded:      false,
		},
		{
			name:                  "TTL of one",
			TTL:                   1,
			srcAddr:               remoteIPv4Addr1,
			dstAddr:               remoteIPv4Addr2,
			expectPacketForwarded: true,
		},
		{
			name:                  "TTL of two",
			TTL:                   2,
			srcAddr:               remoteIPv4Addr1,
			dstAddr:               remoteIPv4Addr2,
			expectPacketForwarded: true,
		},
		{
			name:                  "Max TTL",
			TTL:                   math.MaxUint8,
			srcAddr:               remoteIPv4Addr1,
			dstAddr:               remoteIPv4Addr2,
			expectPacketForwarded: true,
		},
		{
			name:                  "four EOL options",
			TTL:                   2,
			srcAddr:               remoteIPv4Addr1,
			dstAddr:               remoteIPv4Addr2,
			options:               header.IPv4Options{0, 0, 0, 0},
			forwardedOptions:      header.IPv4Options{0, 0, 0, 0},
			expectPacketForwarded: true,
		},
		{
			name:    "TS type 1 full",
			TTL:     2,
			srcAddr: remoteIPv4Addr1,
			dstAddr: remoteIPv4Addr2,
			options: header.IPv4Options{
				68, 12, 13, 0xF1,
				192, 168, 1, 12,
				1, 2, 3, 4,
			},
			icmpError: &icmpError{
				icmpType: header.ICMPv4ParamProblem,
				icmpCode: header.ICMPv4UnusedCode,
			},
			expectedMalformedPacketErrors: 1,
		},
		{
			name:    "TS type 0",
			TTL:     2,
			srcAddr: remoteIPv4Addr1,
			dstAddr: remoteIPv4Addr2,
			options: header.IPv4Options{
				68, 24, 21, 0x00,
				1, 2, 3, 4,
				5, 6, 7, 8,
				9, 10, 11, 12,
				13, 14, 15, 16,
				0, 0, 0, 0,
			},
			forwardedOptions: header.IPv4Options{
				68, 24, 25, 0x00,
				1, 2, 3, 4,
				5, 6, 7, 8,
				9, 10, 11, 12,
				13, 14, 15, 16,
				0x00, 0xad, 0x1c, 0x40, // time we expect from fakeclock
			},
			expectPacketForwarded: true,
		},
		{
			name:    "end of options list",
			TTL:     2,
			srcAddr: remoteIPv4Addr1,
			dstAddr: remoteIPv4Addr2,
			options: header.IPv4Options{
				68, 12, 13, 0x11,
				192, 168, 1, 12,
				1, 2, 3, 4,
				0, 10, 3, 99, // EOL followed by junk
				1, 2, 3, 4,
			},
			forwardedOptions: header.IPv4Options{
				68, 12, 13, 0x21,
				192, 168, 1, 12,
				1, 2, 3, 4,
				0,       // End of Options hides following bytes.
				0, 0, 0, // 7 bytes unknown option removed.
				0, 0, 0, 0,
			},
			expectPacketForwarded: true,
		},
		{
			name:    "Network unreachable",
			TTL:     2,
			srcAddr: remoteIPv4Addr1,
			dstAddr: unreachableIPv4Addr,
			icmpError: &icmpError{
				icmpType: header.ICMPv4DstUnreachable,
				icmpCode: header.ICMPv4NetUnreachable,
			},
			expectedPacketUnrouteableErrors: 1,
			expectPacketForwarded:           false,
		},
		{
			name:                             "unspecified source",
			TTL:                              2,
			srcAddr:                          header.IPv4Any,
			dstAddr:                          remoteIPv4Addr2,
			expectedInitializingSourceErrors: 1,
			expectPacketForwarded:            false,
		},
		{
			name:                             "initializing source",
			TTL:                              2,
			srcAddr:                          tcpip.AddrFromSlice(net.ParseIP("0.0.0.255").To4()),
			dstAddr:                          remoteIPv4Addr2,
			expectedInitializingSourceErrors: 1,
			expectPacketForwarded:            false,
		},
		{
			name:                                 "close outgoing endpoint",
			TTL:                                  2,
			srcAddr:                              remoteIPv4Addr1,
			dstAddr:                              remoteIPv4Addr2,
			closeOutgoingEndpoint:                true,
			expectedOutgoingEndpointClosedErrors: 1,
			expectPacketForwarded:                false,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()
			s := ctx.s
			clock := ctx.clock

			// Advance the clock by some unimportant amount to make
			// it give a more recognisable signature than 00,00,00,00.
			clock.Advance(time.Millisecond * randomTimeOffset)

			endpoints := make(map[tcpip.NICID]*channel.Endpoint)
			for nicID, addr := range defaultEndpointConfigs {
				ep := channel.New(1, ipv4.MaxTotalSize, "")
				defer ep.Close()

				if err := s.CreateNIC(nicID, ep); err != nil {
					t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err)
				}
				addr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: addr}
				if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil {
					t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, addr, err)
				}
				endpoints[nicID] = ep
			}

			s.SetRouteTable([]tcpip.Route{
				{
					Destination: incomingIPv4Addr.Subnet(),
					NIC:         incomingNICID,
				},
				{
					Destination: outgoingIPv4Addr.Subnet(),
					NIC:         outgoingNICID,
				},
			})

			if err := s.SetForwardingDefaultAndAllNICs(header.IPv4ProtocolNumber, true); err != nil {
				t.Fatalf("s.SetForwardingDefaultAndAllNICs(%d, true): %s", header.IPv4ProtocolNumber, err)
			}

			requestPkt, expectedICMPErrorPayload := newICMPEchoPacket(t, test.srcAddr, test.dstAddr, test.TTL, packetOptions{options: test.options})
			defer requestPkt.DecRef()

			outgoingEndpoint, ok := endpoints[outgoingNICID]
			if !ok {
				t.Fatalf("endpoints[%d] = (_, false), want (_, true)", outgoingNICID)
			}

			if test.closeOutgoingEndpoint {
				outgoingEndpoint.Close()
			}

			incomingEndpoint, ok := endpoints[incomingNICID]
			if !ok {
				t.Fatalf("endpoints[%d] = (_, false), want (_, true)", incomingNICID)
			}
			incomingEndpoint.InjectInbound(header.IPv4ProtocolNumber, requestPkt)
			reply := incomingEndpoint.Read()

			if test.icmpError != nil {
				if reply == nil {
					t.Fatalf("Expected ICMP packet type %d through incoming NIC", test.icmpError.icmpType)
				}

				payload := stack.PayloadSince(reply.NetworkHeader())
				defer payload.Release()
				checker.IPv4(t, payload,
					checker.SrcAddr(incomingIPv4Addr.Address),
					checker.DstAddr(test.srcAddr),
					checker.TTL(ipv4.DefaultTTL),
					checker.ICMPv4(
						checker.ICMPv4Checksum(),
						checker.ICMPv4Type(test.icmpError.icmpType),
						checker.ICMPv4Code(test.icmpError.icmpCode),
						checker.ICMPv4Payload(expectedICMPErrorPayload),
					),
				)
				reply.DecRef()
			} else if reply != nil {
				t.Fatalf("Expected no ICMP packet through incoming NIC, instead found: %#v", reply)
			}

			if test.expectPacketForwarded {
				reply := outgoingEndpoint.Read()
				if reply == nil {
					t.Fatal("Expected ICMP Echo packet through outgoing NIC")
				}

				payload := stack.PayloadSince(reply.NetworkHeader())
				defer payload.Release()
				checker.IPv4(t, payload,
					checker.SrcAddr(test.srcAddr),
					checker.DstAddr(test.dstAddr),
					checker.TTL(test.TTL-1),
					checker.IPv4Options(test.forwardedOptions),
					checker.ICMPv4(
						checker.ICMPv4Checksum(),
						checker.ICMPv4Type(header.ICMPv4Echo),
						checker.ICMPv4Code(header.ICMPv4UnusedCode),
						checker.ICMPv4Payload(nil),
					),
				)
				reply.DecRef()
			} else {
				if reply := outgoingEndpoint.Read(); reply != nil {
					t.Fatalf("Expected no ICMP Echo packet through outgoing NIC, instead found: %#v", reply)
				}
			}

			if got, want := s.Stats().IP.Forwarding.InitializingSource.Value(), test.expectedInitializingSourceErrors; got != want {
				t.Errorf("s.Stats().IP.Forwarding.InitializingSource.Value() = %d, want = %d", got, want)
			}

			if got, want := s.Stats().IP.Forwarding.LinkLocalSource.Value(), test.expectedLinkLocalSourceErrors; got != want {
				t.Errorf("s.Stats().IP.Forwarding.LinkLocalSource.Value() = %d, want = %d", got, want)
			}

			if got, want := s.Stats().IP.Forwarding.LinkLocalDestination.Value(), test.expectedLinkLocalDestErrors; got != want {
				t.Errorf("s.Stats().IP.Forwarding.LinkLocalDestination.Value() = %d, want = %d", got, want)
			}

			if got, want := s.Stats().IP.MalformedPacketsReceived.Value(), test.expectedMalformedPacketErrors; got != want {
				t.Errorf("s.Stats().IP.MalformedPacketsReceived.Value() = %d, want = %d", got, want)
			}

			if got, want := s.Stats().IP.Forwarding.ExhaustedTTL.Value(), test.expectedExhaustedTTLErrors; got != want {
				t.Errorf("s.Stats().IP.Forwarding.ExhaustedTTL.Value() = %d, want = %d", got, want)
			}

			if got, want := s.Stats().IP.Forwarding.Unrouteable.Value(), test.expectedPacketUnrouteableErrors; got != want {
				t.Errorf("s.Stats().IP.Forwarding.Unrouteable.Value() = %d, want = %d", got, want)
			}

			if got, want := s.Stats().IP.Forwarding.OutgoingDeviceClosedForSend.Value(), test.expectedOutgoingEndpointClosedErrors; got != want {
				t.Errorf("s.Stats().IP.Forwarding.OutgoingDeviceClosedForSend.Value() = %d, want = %d", got, want)
			}

			expectedTotalErrors := test.expectedLinkLocalSourceErrors + test.expectedLinkLocalDestErrors + test.expectedMalformedPacketErrors + test.expectedExhaustedTTLErrors + test.expectedPacketUnrouteableErrors + test.expectedInitializingSourceErrors + test.expectedOutgoingEndpointClosedErrors
			if got, want := s.Stats().IP.Forwarding.Errors.Value(), expectedTotalErrors; got != want {
				t.Errorf("s.Stats().IP.Forwarding.Errors.Value() = %d, want = %d", got, want)
			}
		})
	}
}

func TestFragmentForwarding(t *testing.T) {
	const (
		defaultMTU           = 1000
		defaultPayloadLength = defaultMTU + 4
		packetTTL            = 2
	)

	tests := []struct {
		name                       string
		ipFlags                    uint8
		icmpError                  *icmpError
		expectedPacketTooBigErrors uint64
		expectedFragmentsForwarded []fragmentInfo
	}{
		{
			name:    "Fragmentation needed and DF set",
			ipFlags: header.IPv4FlagDontFragment,
			// We've picked this MTU because it is:
			//
			// 1) Greater than the minimum MTU that IPv4 hosts are required to process
			// (576 bytes). As per RFC 1812, Section 4.3.2.3:
			//
			//   The ICMP datagram SHOULD contain as much of the original datagram as
			//   possible without the length of the ICMP datagram exceeding 576 bytes.
			//
			// Therefore, setting an MTU greater than 576 bytes ensures that we can fit a
			// complete ICMP packet on the incoming endpoint (and make assertions about
			// it).
			//
			// 2) Less than `ipv4.MaxTotalSize`, which lets us build an IPv4 packet whose
			// size exceeds the MTU.
			icmpError: &icmpError{
				icmpType: header.ICMPv4DstUnreachable,
				icmpCode: header.ICMPv4FragmentationNeeded,
			},
			expectedFragmentsForwarded: []fragmentInfo{},
			expectedPacketTooBigErrors: 1,
		},
		{
			name: "Fragmentation needed and DF not set",
			expectedFragmentsForwarded: []fragmentInfo{
				// The first fragment has a length of the greatest multiple of 8 which is
				// less than or equal to to `mtu - header.IPv4MinimumSize`.
				{offset: 0, payloadSize: uint16(976), more: true},
				// The next fragment holds the rest of the packet.
				{offset: uint16(976), payloadSize: 36, more: false},
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()
			s := ctx.s

			endpoints := make(map[tcpip.NICID]*channel.Endpoint)
			for nicID, addr := range defaultEndpointConfigs {
				// For the input interface, we expect at most a single packet in
				// response to our ICMP Echo Request.
				expectedEmittedPacketCount := 1

				if nicID == outgoingNICID {
					expectedEmittedPacketCount = max(1, len(test.expectedFragmentsForwarded))
				}

				ep := channel.New(expectedEmittedPacketCount, defaultMTU, "")
				defer ep.Close()

				if err := s.CreateNIC(nicID, ep); err != nil {
					t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err)
				}
				addr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: addr}
				if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil {
					t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, addr, err)
				}
				endpoints[nicID] = ep
			}

			s.SetRouteTable([]tcpip.Route{
				{
					Destination: incomingIPv4Addr.Subnet(),
					NIC:         incomingNICID,
				},
				{
					Destination: outgoingIPv4Addr.Subnet(),
					NIC:         outgoingNICID,
				},
			})

			if err := s.SetForwardingDefaultAndAllNICs(header.IPv4ProtocolNumber, true); err != nil {
				t.Fatalf("s.SetForwardingDefaultAndAllNICs(%d, true): %s", header.IPv4ProtocolNumber, err)
			}

			requestPkt, expectedICMPErrorPayload := newICMPEchoPacket(t, remoteIPv4Addr1, remoteIPv4Addr2, packetTTL, packetOptions{ipFlags: test.ipFlags, payloadLength: defaultPayloadLength})
			defer requestPkt.DecRef()

			incomingEndpoint, ok := endpoints[incomingNICID]
			if !ok {
				t.Fatalf("endpoints[%d] = (_, false), want (_, true)", incomingNICID)
			}
			incomingEndpoint.InjectInbound(header.IPv4ProtocolNumber, requestPkt)
			reply := incomingEndpoint.Read()

			if test.icmpError != nil {
				if reply == nil {
					t.Fatalf("Expected ICMP packet type %d through incoming NIC", test.icmpError.icmpType)
				}

				payload := stack.PayloadSince(reply.NetworkHeader())
				defer payload.Release()
				checker.IPv4(t, payload,
					checker.SrcAddr(incomingIPv4Addr.Address),
					checker.DstAddr(remoteIPv4Addr1),
					checker.TTL(ipv4.DefaultTTL),
					checker.ICMPv4(
						checker.ICMPv4Checksum(),
						checker.ICMPv4Type(test.icmpError.icmpType),
						checker.ICMPv4Code(test.icmpError.icmpCode),
						checker.ICMPv4Payload(expectedICMPErrorPayload),
					),
				)
				reply.DecRef()
			} else if reply != nil {
				t.Fatalf("Expected no ICMP packet through incoming NIC, instead found: %#v", reply)
			}

			outgoingEndpoint, ok := endpoints[outgoingNICID]
			if !ok {
				t.Fatalf("endpoints[%d] = (_, false), want (_, true)", outgoingNICID)
			}

			if len(test.expectedFragmentsForwarded) > 0 {
				checkFragements(t, outgoingEndpoint, test.expectedFragmentsForwarded, requestPkt)
			} else {
				if reply := outgoingEndpoint.Read(); reply != nil {
					t.Errorf("Expected no ICMP Echo packet through outgoing NIC, instead found: %#v", reply)
				}
			}

			if got, want := s.Stats().IP.Forwarding.PacketTooBig.Value(), test.expectedPacketTooBigErrors; got != want {
				t.Errorf("s.Stats().IP.Forwarding.PacketTooBig.Value() = %d, want = %d", got, want)
			}

			if got, want := s.Stats().IP.Forwarding.Errors.Value(), test.expectedPacketTooBigErrors; got != want {
				t.Errorf("s.Stats().IP.Forwarding.Errors.Value() = %d, want = %d", got, want)
			}
		})
	}
}

func TestMulticastFragmentForwarding(t *testing.T) {
	const (
		defaultMTU           = 1000
		defaultPayloadLength = defaultMTU + 4
		packetTTL            = 2
		multicastRouteMinTTL = 2
	)

	tests := []struct {
		name                       string
		ipFlags                    uint8
		icmpError                  *icmpError
		expectedPacketTooBigErrors uint64
		expectedFragmentsForwarded []fragmentInfo
	}{
		{
			name:    "Fragmentation needed and DF set",
			ipFlags: header.IPv4FlagDontFragment,
			// We've picked this MTU because it is:
			//
			// 1) Greater than the minimum MTU that IPv4 hosts are required to process
			// (576 bytes). As per RFC 1812, Section 4.3.2.3:
			//
			//   The ICMP datagram SHOULD contain as much of the original datagram as
			//   possible without the length of the ICMP datagram exceeding 576 bytes.
			//
			// Therefore, setting an MTU greater than 576 bytes ensures that we can fit a
			// complete ICMP packet on the incoming endpoint (and make assertions about
			// it).
			//
			// 2) Less than `ipv4.MaxTotalSize`, which lets us build an IPv4 packet whose
			// size exceeds the MTU.
			icmpError: &icmpError{
				icmpType: header.ICMPv4DstUnreachable,
				icmpCode: header.ICMPv4FragmentationNeeded,
			},
			expectedFragmentsForwarded: []fragmentInfo{},
			expectedPacketTooBigErrors: 1,
		},
		{
			name: "Fragmentation needed and DF not set",
			expectedFragmentsForwarded: []fragmentInfo{
				// The first fragment has a length of the greatest multiple of 8 which is
				// less than or equal to to `mtu - header.IPv4MinimumSize`.
				{offset: 0, payloadSize: uint16(976), more: true},
				// The next fragment holds the rest of the packet.
				{offset: uint16(976), payloadSize: 36, more: false},
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()
			s := ctx.s

			if _, err := s.EnableMulticastForwardingForProtocol(ipv4.ProtocolNumber, &fakeMulticastEventDispatcher{}); err != nil {
				t.Fatalf("s.EnableMulticastForwardingForProtocol(%d, _): (_, %s)", ipv4.ProtocolNumber, err)
			}

			endpoints := make(map[tcpip.NICID]*channel.Endpoint)
			for nicID, addr := range defaultEndpointConfigs {
				// For the input interface, we expect at most a single packet in
				// response to our ICMP Echo Request.
				expectedEmittedPacketCount := 1

				if nicID == outgoingNICID {
					expectedEmittedPacketCount = max(1, len(test.expectedFragmentsForwarded))
				}

				ep := channel.New(expectedEmittedPacketCount, defaultMTU, "")
				defer ep.Close()

				if err := s.CreateNIC(nicID, ep); err != nil {
					t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err)
				}
				addr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: addr}
				if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil {
					t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, addr, err)
				}
				s.SetNICMulticastForwarding(nicID, ipv4.ProtocolNumber, true /* enabled */)
				endpoints[nicID] = ep
			}

			// Add a route that could theoretically be used to send an ICMP error.
			// Note that such an error should never be sent for multicast.
			s.SetRouteTable([]tcpip.Route{
				{
					Destination: header.IPv4EmptySubnet,
					NIC:         outgoingNICID,
				},
			})

			outgoingInterfaces := []stack.MulticastRouteOutgoingInterface{
				{ID: outgoingNICID, MinTTL: multicastRouteMinTTL},
			}
			addresses := stack.UnicastSourceAndMulticastDestination{
				Source:      remoteIPv4Addr1,
				Destination: multicastIPv4Addr,
			}

			route := stack.MulticastRoute{
				ExpectedInputInterface: incomingNICID,
				OutgoingInterfaces:     outgoingInterfaces,
			}

			if err := s.AddMulticastRoute(ipv4.ProtocolNumber, addresses, route); err != nil {
				t.Fatalf("s.AddMulticastRoute(%d, %#v, %#v): %s", ipv4.ProtocolNumber, addresses, route, err)
			}

			requestPkt, _ := newICMPEchoPacket(t, remoteIPv4Addr1, multicastIPv4Addr, packetTTL, packetOptions{ipFlags: test.ipFlags, payloadLength: defaultPayloadLength})
			defer requestPkt.DecRef()

			incomingEndpoint, ok := endpoints[incomingNICID]
			if !ok {
				t.Fatalf("got endpoints[%d] = (_, false), want (_, true)", incomingNICID)
			}
			incomingEndpoint.InjectInbound(header.IPv4ProtocolNumber, requestPkt)
			reply := incomingEndpoint.Read()

			if reply != nil {
				// An ICMP error should never be sent in response to a multicast packet.
				t.Errorf("Expected no ICMP packet through incoming NIC, instead found: %#v", reply)
			}

			outgoingEndpoint, ok := endpoints[outgoingNICID]
			if !ok {
				t.Fatalf("endpoints[%d] = (_, false), want (_, true)", outgoingNICID)
			}

			if len(test.expectedFragmentsForwarded) > 0 {
				checkFragements(t, outgoingEndpoint, test.expectedFragmentsForwarded, requestPkt)
			} else {
				if reply := outgoingEndpoint.Read(); reply != nil {
					t.Errorf("Expected no ICMP Echo packet through outgoing NIC, instead found: %#v", reply)
				}
			}

			if got, want := s.Stats().IP.Forwarding.PacketTooBig.Value(), test.expectedPacketTooBigErrors; got != want {
				t.Errorf("s.Stats().IP.Forwarding.PacketTooBig.Value() = %d, want = %d", got, want)
			}

			if got, want := s.Stats().IP.Forwarding.Errors.Value(), test.expectedPacketTooBigErrors; got != want {
				t.Errorf("s.Stats().IP.Forwarding.Errors.Value() = %d, want = %d", got, want)
			}
		})
	}
}

func TestMulticastForwardingOptions(t *testing.T) {
	const (
		randomTimeOffset     = 0x10203040
		packetTTL            = 2
		multicastRouteMinTTL = 2
	)

	tests := []struct {
		name                          string
		options                       header.IPv4Options
		forwardedOptions              header.IPv4Options
		expectedMalformedPacketErrors uint64
		expectPacketForwarded         bool
	}{
		{
			name:                  "four EOL options",
			options:               header.IPv4Options{0, 0, 0, 0},
			forwardedOptions:      header.IPv4Options{0, 0, 0, 0},
			expectPacketForwarded: true,
		},
		{
			name: "TS type 1 full",
			options: header.IPv4Options{
				68, 12, 13, 0xF1,
				192, 168, 1, 12,
				1, 2, 3, 4,
			},
			expectedMalformedPacketErrors: 1,
		},
		{
			name: "TS type 0",
			options: header.IPv4Options{
				68, 24, 21, 0x00,
				1, 2, 3, 4,
				5, 6, 7, 8,
				9, 10, 11, 12,
				13, 14, 15, 16,
				0, 0, 0, 0,
			},
			forwardedOptions: header.IPv4Options{
				68, 24, 25, 0x00,
				1, 2, 3, 4,
				5, 6, 7, 8,
				9, 10, 11, 12,
				13, 14, 15, 16,
				0x00, 0xad, 0x1c, 0x40, // time we expect from fakeclock
			},
			expectPacketForwarded: true,
		},
		{
			name: "end of options list",
			options: header.IPv4Options{
				68, 12, 13, 0x11,
				192, 168, 1, 12,
				1, 2, 3, 4,
				0, 10, 3, 99, // EOL followed by junk
				1, 2, 3, 4,
			},
			forwardedOptions: header.IPv4Options{
				68, 12, 13, 0x21,
				192, 168, 1, 12,
				1, 2, 3, 4,
				0,       // End of Options hides following bytes.
				0, 0, 0, // 7 bytes unknown option removed.
				0, 0, 0, 0,
			},
			expectPacketForwarded: true,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()
			s := ctx.s
			clock := ctx.clock

			// Advance the clock by some unimportant amount to make
			// it give a more recognisable signature than 00,00,00,00.
			clock.Advance(time.Millisecond * randomTimeOffset)

			if _, err := s.EnableMulticastForwardingForProtocol(ipv4.ProtocolNumber, &fakeMulticastEventDispatcher{}); err != nil {
				t.Fatalf("s.EnableMulticastForwardingForProtocol(%d, _): (_, %s)", ipv4.ProtocolNumber, err)
			}

			endpoints := make(map[tcpip.NICID]*channel.Endpoint)
			for nicID, addr := range defaultEndpointConfigs {
				ep := channel.New(1, ipv4.MaxTotalSize, "")
				defer ep.Close()

				if err := s.CreateNIC(nicID, ep); err != nil {
					t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err)
				}
				addr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: addr}
				if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil {
					t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, addr, err)
				}
				s.SetNICMulticastForwarding(nicID, ipv4.ProtocolNumber, true /* enabled */)
				endpoints[nicID] = ep
			}

			// Add a route that could theoretically be used to send an ICMP error.
			// Note that such an error should never be sent for multicast.
			s.SetRouteTable([]tcpip.Route{
				{
					Destination: header.IPv4EmptySubnet,
					NIC:         outgoingNICID,
				},
			})

			outgoingInterfaces := []stack.MulticastRouteOutgoingInterface{
				{ID: outgoingNICID, MinTTL: multicastRouteMinTTL},
			}
			addresses := stack.UnicastSourceAndMulticastDestination{
				Source:      remoteIPv4Addr1,
				Destination: multicastIPv4Addr,
			}

			route := stack.MulticastRoute{
				ExpectedInputInterface: incomingNICID,
				OutgoingInterfaces:     outgoingInterfaces,
			}

			if err := s.AddMulticastRoute(ipv4.ProtocolNumber, addresses, route); err != nil {
				t.Fatalf("s.AddMulticastRoute(%d, %#v, %#v): %s", ipv4.ProtocolNumber, addresses, route, err)
			}

			requestPkt, _ := newICMPEchoPacket(t, remoteIPv4Addr1, multicastIPv4Addr, packetTTL, packetOptions{options: test.options})
			defer requestPkt.DecRef()

			incomingEndpoint, ok := endpoints[incomingNICID]
			if !ok {
				t.Fatalf("endpoints[%d] = (_, false), want (_, true)", incomingNICID)
			}
			incomingEndpoint.InjectInbound(header.IPv4ProtocolNumber, requestPkt)
			reply := incomingEndpoint.Read()

			if reply != nil {
				// An ICMP error should never be sent in response to a multicast packet.
				t.Errorf("Expected no ICMP packet through incoming NIC, instead found: %#v", reply)
			}

			outgoingEndpoint, ok := endpoints[outgoingNICID]
			if !ok {
				t.Fatalf("endpoints[%d] = (_, false), want (_, true)", outgoingNICID)
			}

			if test.expectPacketForwarded {
				reply := outgoingEndpoint.Read()
				if reply == nil {
					t.Fatal("Expected ICMP Echo packet through outgoing NIC")
				}

				payload := stack.PayloadSince(reply.NetworkHeader())
				defer payload.Release()
				checker.IPv4(t, payload,
					checker.SrcAddr(remoteIPv4Addr1),
					checker.DstAddr(multicastIPv4Addr),
					checker.TTL(packetTTL-1),
					checker.IPv4Options(test.forwardedOptions),
					checker.ICMPv4(
						checker.ICMPv4Checksum(),
						checker.ICMPv4Type(header.ICMPv4Echo),
						checker.ICMPv4Code(header.ICMPv4UnusedCode),
						checker.ICMPv4Payload(nil),
					),
				)
				reply.DecRef()
			} else {
				if reply := outgoingEndpoint.Read(); reply != nil {
					t.Fatalf("Expected no ICMP Echo packet through outgoing NIC, instead found: %#v", reply)
				}
			}

			if got, want := s.Stats().IP.MalformedPacketsReceived.Value(), test.expectedMalformedPacketErrors; got != want {
				t.Errorf("s.Stats().IP.MalformedPacketsReceived.Value() = %d, want = %d", got, want)
			}

			if got, want := s.Stats().IP.Forwarding.Errors.Value(), test.expectedMalformedPacketErrors; got != want {
				t.Errorf("s.Stats().IP.Forwarding.Errors.Value() = %d, want = %d", got, want)
			}
		})
	}
}

// TestIPv4Sanity sends IP/ICMP packets with various problems to the stack and
// checks the response.
func TestIPv4Sanity(t *testing.T) {
	const (
		ttl            = 255
		nicID          = 1
		randomSequence = 123
		randomIdent    = 42
		// In some cases Linux sets the error pointer to the start of the option
		// (offset 0) instead of the actual wrong value, which is the length byte
		// (offset 1). For compatibility we must do the same. Use this constant
		// to indicate where this happens.
		pointerOffsetForInvalidLength = 0
		randomTimeOffset              = 0x10203040
	)
	var (
		ipv4Addr = tcpip.AddressWithPrefix{
			Address:   tcpip.AddrFromSlice(net.ParseIP("192.168.1.58").To4()),
			PrefixLen: 24,
		}
		remoteIPv4Addr = tcpip.AddrFromSlice(net.ParseIP("10.0.0.1").To4())
	)

	tests := []struct {
		name                string
		headerLength        uint8 // value of 0 means "use correct size"
		badHeaderChecksum   bool
		maxTotalLength      uint16
		transportProtocol   uint8
		TTL                 uint8
		options             header.IPv4Options
		replyOptions        header.IPv4Options // reply should look like this
		shouldFail          bool
		expectErrorICMP     bool
		ICMPType            header.ICMPv4Type
		ICMPCode            header.ICMPv4Code
		paramProblemPointer uint8
	}{
		{
			name:              "valid no options",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
		},
		{
			name:              "bad header checksum",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			badHeaderChecksum: true,
			shouldFail:        true,
		},
		// The TTL tests check that we are not rejecting an incoming packet
		// with a zero or one TTL, which has been a point of confusion in the
		// past as RFC 791 says: "If this field contains the value zero, then the
		// datagram must be destroyed". However RFC 1122 section 3.2.1.7 clarifies
		// for the case of the destination host, stating as follows.
		//
		//      A host MUST NOT send a datagram with a Time-to-Live (TTL)
		//      value of zero.
		//
		//      A host MUST NOT discard a datagram just because it was
		//      received with TTL less than 2.
		{
			name:              "zero TTL",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               0,
		},
		{
			name:              "one TTL",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               1,
		},
		{
			name:              "End options",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options:           header.IPv4Options{0, 0, 0, 0},
			replyOptions:      header.IPv4Options{0, 0, 0, 0},
		},
		{
			name:              "NOP options",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options:           header.IPv4Options{1, 1, 1, 1},
			replyOptions:      header.IPv4Options{1, 1, 1, 1},
		},
		{
			name:              "NOP and End options",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options:           header.IPv4Options{1, 1, 0, 0},
			replyOptions:      header.IPv4Options{1, 1, 0, 0},
		},
		{
			name:              "bad header length",
			headerLength:      header.IPv4MinimumSize - 1,
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			shouldFail:        true,
		},
		{
			name:              "bad total length (0)",
			maxTotalLength:    0,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			shouldFail:        true,
		},
		{
			name:              "bad total length (ip - 1)",
			maxTotalLength:    uint16(header.IPv4MinimumSize - 1),
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			shouldFail:        true,
		},
		{
			name:              "bad total length (ip + icmp - 1)",
			maxTotalLength:    uint16(header.IPv4MinimumSize + header.ICMPv4MinimumSize - 1),
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			shouldFail:        true,
		},
		{
			name:              "bad protocol",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: 99,
			TTL:               ttl,
			shouldFail:        true,
			expectErrorICMP:   true,
			ICMPType:          header.ICMPv4DstUnreachable,
			ICMPCode:          header.ICMPv4ProtoUnreachable,
		},
		{
			name:              "timestamp option overflow",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 12, 13, 0x11,
				192, 168, 1, 12,
				1, 2, 3, 4,
			},
			replyOptions: header.IPv4Options{
				68, 12, 13, 0x21,
				192, 168, 1, 12,
				1, 2, 3, 4,
			},
		},
		{
			name:              "timestamp option overflow full",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 12, 13, 0xF1,
				//            ^   Counter full (15/0xF)
				192, 168, 1, 12,
				1, 2, 3, 4,
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + 3,
			replyOptions:        header.IPv4Options{},
		},
		{
			name:              "unknown option",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options:           header.IPv4Options{10, 4, 9, 0},
			//                        ^^
			// The unknown option should be stripped out of the reply.
			replyOptions: header.IPv4Options{},
		},
		{
			name:              "bad option - no length",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				1, 1, 1, 68,
				//        ^-start of timestamp.. but no length..
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + 3,
		},
		{
			name:              "bad option - length 0",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 0, 9, 0,
				//  ^
				1, 2, 3, 4,
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + pointerOffsetForInvalidLength,
		},
		{
			name:              "bad option - length 1",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 1, 9, 0,
				//  ^
				1, 2, 3, 4,
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + pointerOffsetForInvalidLength,
		},
		{
			name:              "bad option - length big",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 9, 9, 0,
				//  ^
				// There are only 8 bytes allocated to options so 9 bytes of timestamp
				// space is not possible. (Second byte)
				1, 2, 3, 4,
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + pointerOffsetForInvalidLength,
		},
		{
			// This tests for some linux compatible behaviour.
			// The ICMP pointer returned is 22 for Linux but the
			// error is actually in spot 21.
			name:              "bad option - length bad",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			// Timestamps are in multiples of 4 or 8 but never 7.
			// The option space should be padded out.
			options: header.IPv4Options{
				68, 7, 5, 0,
				//  ^  ^ Linux points here which is wrong.
				//  | Not a multiple of 4
				1, 2, 3, 0,
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + header.IPv4OptTSPointerOffset,
		},
		{
			name:              "multiple type 0 with room",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 24, 21, 0x00,
				1, 2, 3, 4,
				5, 6, 7, 8,
				9, 10, 11, 12,
				13, 14, 15, 16,
				0, 0, 0, 0,
			},
			replyOptions: header.IPv4Options{
				68, 24, 25, 0x00,
				1, 2, 3, 4,
				5, 6, 7, 8,
				9, 10, 11, 12,
				13, 14, 15, 16,
				0x00, 0xad, 0x1c, 0x40, // time we expect from fakeclock
			},
		},
		{
			// The timestamp area is full so add to the overflow count.
			name:              "multiple type 1 timestamps",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 20, 21, 0x11,
				//            ^
				192, 168, 1, 12,
				1, 2, 3, 4,
				192, 168, 1, 13,
				5, 6, 7, 8,
			},
			// Overflow count is the top nibble of the 4th byte.
			replyOptions: header.IPv4Options{
				68, 20, 21, 0x21,
				//            ^
				192, 168, 1, 12,
				1, 2, 3, 4,
				192, 168, 1, 13,
				5, 6, 7, 8,
			},
		},
		{
			name:              "multiple type 1 timestamps with room",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 28, 21, 0x01,
				192, 168, 1, 12,
				1, 2, 3, 4,
				192, 168, 1, 13,
				5, 6, 7, 8,
				0, 0, 0, 0,
				0, 0, 0, 0,
			},
			replyOptions: header.IPv4Options{
				68, 28, 29, 0x01,
				192, 168, 1, 12,
				1, 2, 3, 4,
				192, 168, 1, 13,
				5, 6, 7, 8,
				192, 168, 1, 58, // New IP Address.
				0x00, 0xad, 0x1c, 0x40, // time we expect from fakeclock
			},
		},
		{
			// Timestamp pointer uses one based counting so 0 is invalid.
			name:              "timestamp pointer invalid",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 8, 0, 0x00,
				//      ^ 0 instead of 5 or more.
				0, 0, 0, 0,
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + 2,
		},
		{
			// Timestamp pointer cannot be less than 5. It must point past the header
			// which is 4 bytes. (1 based counting)
			name:              "timestamp pointer too small by 1",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 8, header.IPv4OptionTimestampHdrLength, 0x00,
				//          ^ header is 4 bytes, so 4 should fail.
				0, 0, 0, 0,
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + header.IPv4OptTSPointerOffset,
		},
		{
			name:              "valid timestamp pointer",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 8, header.IPv4OptionTimestampHdrLength + 1, 0x00,
				//          ^ header is 4 bytes, so 5 should succeed.
				0, 0, 0, 0,
			},
			replyOptions: header.IPv4Options{
				68, 8, 9, 0x00,
				0x00, 0xad, 0x1c, 0x40, // time we expect from fakeclock
			},
		},
		{
			// Needs 8 bytes for a type 1 timestamp but there are only 4 free.
			name:              "bad timer element alignment",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 20, 17, 0x01,
				//  ^^  ^^   20 byte area, next free spot at 17.
				192, 168, 1, 12,
				1, 2, 3, 4,
				0, 0, 0, 0,
				0, 0, 0, 0,
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + header.IPv4OptTSPointerOffset,
		},
		// End of option list with illegal option after it, which should be ignored.
		{
			name:              "end of options list",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 12, 13, 0x11,
				192, 168, 1, 12,
				1, 2, 3, 4,
				0, 10, 3, 99, // EOL followed by junk
			},
			replyOptions: header.IPv4Options{
				68, 12, 13, 0x21,
				192, 168, 1, 12,
				1, 2, 3, 4,
				0,       // End of Options hides following bytes.
				0, 0, 0, // 3 bytes unknown option removed.
			},
		},
		{
			// Timestamp with a size much too small.
			name:              "timestamp truncated",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				68, 1, 0, 0,
				//  ^ Smallest possible is 8. Linux points at the 68.
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + pointerOffsetForInvalidLength,
		},
		{
			name:              "single record route with room",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				7, 7, 4, //  3 byte header
				0, 0, 0, 0,
				0,
			},
			replyOptions: header.IPv4Options{
				7, 7, 8, // 3 byte header
				192, 168, 1, 58, // New IP Address.
				0, // padding to multiple of 4 bytes.
			},
		},
		{
			name:              "multiple record route with room",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				7, 23, 20, //  3 byte header
				1, 2, 3, 4,
				5, 6, 7, 8,
				9, 10, 11, 12,
				13, 14, 15, 16,
				0, 0, 0, 0,
				0,
			},
			replyOptions: header.IPv4Options{
				7, 23, 24,
				1, 2, 3, 4,
				5, 6, 7, 8,
				9, 10, 11, 12,
				13, 14, 15, 16,
				192, 168, 1, 58, // New IP Address.
				0, // padding to multiple of 4 bytes.
			},
		},
		{
			name:              "single record route with no room",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				7, 7, 8, // 3 byte header
				1, 2, 3, 4,
				0,
			},
			replyOptions: header.IPv4Options{
				7, 7, 8, // 3 byte header
				1, 2, 3, 4,
				0, // padding to multiple of 4 bytes.
			},
		},
		{
			// Unlike timestamp, this should just succeed.
			name:              "multiple record route with no room",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				7, 23, 24, // 3 byte header
				1, 2, 3, 4,
				5, 6, 7, 8,
				9, 10, 11, 12,
				13, 14, 15, 16,
				17, 18, 19, 20,
				0,
			},
			replyOptions: header.IPv4Options{
				7, 23, 24,
				1, 2, 3, 4,
				5, 6, 7, 8,
				9, 10, 11, 12,
				13, 14, 15, 16,
				17, 18, 19, 20,
				0, // padding to multiple of 4 bytes.
			},
		},
		{
			// Pointer uses one based counting so 0 is invalid.
			name:              "record route pointer zero",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				7, 8, 0, // 3 byte header
				0, 0, 0, 0,
				0,
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + header.IPv4OptRRPointerOffset,
		},
		{
			// Pointer must be 4 or more as it must point past the 3 byte header
			// using 1 based counting. 3 should fail.
			name:              "record route pointer too small by 1",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				7, 8, header.IPv4OptionRecordRouteHdrLength, // 3 byte header
				0, 0, 0, 0,
				0,
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + header.IPv4OptRRPointerOffset,
		},
		{
			// Pointer must be 4 or more as it must point past the 3 byte header
			// using 1 based counting. Check 4 passes. (Duplicates "single
			// record route with room")
			name:              "valid record route pointer",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				7, 7, header.IPv4OptionRecordRouteHdrLength + 1, // 3 byte header
				0, 0, 0, 0,
				0,
			},
			replyOptions: header.IPv4Options{
				7, 7, 8, // 3 byte header
				192, 168, 1, 58, // New IP Address.
				0, // padding to multiple of 4 bytes.
			},
		},
		{
			// Confirm Linux bug for bug compatibility.
			// Linux returns slot 22 but the error is in slot 21.
			name:              "multiple record route with not enough room",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				7, 8, 8, // 3 byte header
				// ^  ^ Linux points here. We must too.
				// | Not enough room. 1 byte free, need 4.
				1, 2, 3, 4,
				0,
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + header.IPv4OptRRPointerOffset,
		},
		{
			name:              "duplicate record route",
			maxTotalLength:    ipv4.MaxTotalSize,
			transportProtocol: uint8(header.ICMPv4ProtocolNumber),
			TTL:               ttl,
			options: header.IPv4Options{
				7, 7, 8, // 3 byte header
				1, 2, 3, 4,
				7, 7, 8, // 3 byte header
				1, 2, 3, 4,
				0, 0, // pad
			},
			shouldFail:          true,
			expectErrorICMP:     true,
			ICMPType:            header.ICMPv4ParamProblem,
			ICMPCode:            header.ICMPv4UnusedCode,
			paramProblemPointer: header.IPv4MinimumSize + 7,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()
			s := ctx.s
			clock := ctx.clock

			// Advance the clock by some unimportant amount to make
			// it give a more recognisable signature than 00,00,00,00.
			clock.Advance(time.Millisecond * randomTimeOffset)

			// We expect at most a single packet in response to our ICMP Echo Request.
			e := channel.New(1, ipv4.MaxTotalSize, "")
			defer e.Close()
			if err := s.CreateNIC(nicID, e); err != nil {
				t.Fatalf("CreateNIC(%d, _): %s", nicID, err)
			}
			ipv4ProtoAddr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: ipv4Addr}
			if err := s.AddProtocolAddress(nicID, ipv4ProtoAddr, stack.AddressProperties{}); err != nil {
				t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, ipv4ProtoAddr, err)
			}

			// Default routes for IPv4 so ICMP can find a route to the remote
			// node when attempting to send the ICMP Echo Reply.
			s.SetRouteTable([]tcpip.Route{
				{
					Destination: header.IPv4EmptySubnet,
					NIC:         nicID,
				},
			})

			if len(test.options)%4 != 0 {
				t.Fatalf("options must be aligned to 32 bits, invalid test options: %x (len=%d)", test.options, len(test.options))
			}
			ipHeaderLength := header.IPv4MinimumSize + len(test.options)
			if ipHeaderLength > header.IPv4MaximumHeaderSize {
				t.Fatalf("IP header length too large: got = %d, want <= %d ", ipHeaderLength, header.IPv4MaximumHeaderSize)
			}
			totalLen := uint16(ipHeaderLength + header.ICMPv4MinimumSize)
			hdr := prependable.New(int(totalLen))
			icmpH := header.ICMPv4(hdr.Prepend(header.ICMPv4MinimumSize))

			// Specify ident/seq to make sure we get the same in the response.
			icmpH.SetIdent(randomIdent)
			icmpH.SetSequence(randomSequence)
			icmpH.SetType(header.ICMPv4Echo)
			icmpH.SetCode(header.ICMPv4UnusedCode)
			icmpH.SetChecksum(0)
			icmpH.SetChecksum(^checksum.Checksum(icmpH, 0))
			ip := header.IPv4(hdr.Prepend(ipHeaderLength))
			if test.maxTotalLength < totalLen {
				totalLen = test.maxTotalLength
			}
			ip.Encode(&header.IPv4Fields{
				TotalLength: totalLen,
				Protocol:    test.transportProtocol,
				TTL:         test.TTL,
				SrcAddr:     remoteIPv4Addr,
				DstAddr:     ipv4Addr.Address,
			})
			if test.headerLength != 0 {
				ip.SetHeaderLength(test.headerLength)
			} else {
				// Set the calculated header length, since we may manually add options.
				ip.SetHeaderLength(uint8(ipHeaderLength))
			}
			if len(test.options) != 0 {
				// Copy options manually. We do not use Encode for options so we can
				// verify malformed options with handcrafted payloads.
				if want, got := copy(ip.Options(), test.options), len(test.options); want != got {
					t.Fatalf("got copy(ip.Options(), test.options) = %d, want = %d", got, want)
				}
			}
			ip.SetChecksum(0)
			ipHeaderChecksum := ip.CalculateChecksum()
			if test.badHeaderChecksum {
				ipHeaderChecksum += 42
			}
			ip.SetChecksum(^ipHeaderChecksum)
			requestPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
				Payload: buffer.MakeWithData(hdr.View()),
			})
			defer requestPkt.DecRef()
			e.InjectInbound(header.IPv4ProtocolNumber, requestPkt)
			reply := e.Read()
			if reply == nil {
				if test.shouldFail {
					if test.expectErrorICMP {
						t.Fatalf("ICMP error response (type %d, code %d) missing", test.ICMPType, test.ICMPCode)
					}
					return // Expected silent failure.
				}
				t.Fatal("expected ICMP echo reply missing")
			}
			defer reply.DecRef()

			// We didn't expect a packet. Register our surprise but carry on to
			// provide more information about what we got.
			if test.shouldFail && !test.expectErrorICMP {
				t.Error("unexpected packet response")
			}

			// Check the route that brought the packet to us.
			if reply.EgressRoute.LocalAddress != ipv4Addr.Address {
				t.Errorf("got pkt.Route.LocalAddress = %s, want = %s", reply.EgressRoute.LocalAddress, ipv4Addr.Address)
			}
			if reply.EgressRoute.RemoteAddress != remoteIPv4Addr {
				t.Errorf("got pkt.Route.RemoteAddress = %s, want = %s", reply.EgressRoute.RemoteAddress, remoteIPv4Addr)
			}

			// Make sure it's all in one buffer for checker.
			replyIPHeader := stack.PayloadSince(reply.NetworkHeader())
			defer replyIPHeader.Release()

			// At this stage we only know it's probably an IP+ICMP header so verify
			// that much.
			checker.IPv4(t, replyIPHeader,
				checker.SrcAddr(ipv4Addr.Address),
				checker.DstAddr(remoteIPv4Addr),
				checker.ICMPv4(
					checker.ICMPv4Checksum(),
				),
			)

			// Don't proceed any further if the checker found problems.
			if t.Failed() {
				t.FailNow()
			}

			// OK it's ICMP. We can safely look at the type now.
			replyICMPHeader := header.ICMPv4(header.IPv4(replyIPHeader.AsSlice()).Payload())
			switch replyICMPHeader.Type() {
			case header.ICMPv4ParamProblem:
				if !test.shouldFail {
					t.Fatalf("got Parameter Problem with pointer %d, wanted Echo Reply", replyICMPHeader.Pointer())
				}
				if !test.expectErrorICMP {
					t.Fatalf("got Parameter Problem with pointer %d, wanted no response", replyICMPHeader.Pointer())
				}
				checker.IPv4(t, replyIPHeader,
					checker.IPFullLength(uint16(header.IPv4MinimumSize+header.ICMPv4MinimumSize+requestPkt.Size())),
					checker.IPv4HeaderLength(header.IPv4MinimumSize),
					checker.ICMPv4(
						checker.ICMPv4Type(test.ICMPType),
						checker.ICMPv4Code(test.ICMPCode),
						checker.ICMPv4Pointer(test.paramProblemPointer),
						checker.ICMPv4Payload(hdr.View()),
					),
				)
				return
			case header.ICMPv4DstUnreachable:
				if !test.shouldFail {
					t.Fatalf("got ICMP error packet type %d, code %d, wanted Echo Reply",
						header.ICMPv4DstUnreachable, replyICMPHeader.Code())
				}
				if !test.expectErrorICMP {
					t.Fatalf("got ICMP error packet type %d, code %d, wanted no response",
						header.ICMPv4DstUnreachable, replyICMPHeader.Code())
				}
				checker.IPv4(t, replyIPHeader,
					checker.IPFullLength(uint16(header.IPv4MinimumSize+header.ICMPv4MinimumSize+requestPkt.Size())),
					checker.IPv4HeaderLength(header.IPv4MinimumSize),
					checker.ICMPv4(
						checker.ICMPv4Type(test.ICMPType),
						checker.ICMPv4Code(test.ICMPCode),
						checker.ICMPv4Payload(hdr.View()),
					),
				)
				return
			case header.ICMPv4EchoReply:
				if test.shouldFail {
					if !test.expectErrorICMP {
						t.Error("got Echo Reply packet, want no response")
					} else {
						t.Errorf("got Echo Reply, want ICMP error type %d, code %d", test.ICMPType, test.ICMPCode)
					}
				}
				// If the IP options change size then the packet will change size, so
				// some IP header fields will need to be adjusted for the checks.
				sizeChange := len(test.replyOptions) - len(test.options)

				checker.IPv4(t, replyIPHeader,
					checker.IPv4HeaderLength(ipHeaderLength+sizeChange),
					checker.IPv4Options(test.replyOptions),
					checker.IPFullLength(uint16(requestPkt.Size()+sizeChange)),
					checker.ICMPv4(
						checker.ICMPv4Checksum(),
						checker.ICMPv4Code(header.ICMPv4UnusedCode),
						checker.ICMPv4Seq(randomSequence),
						checker.ICMPv4Ident(randomIdent),
					),
				)
			default:
				t.Fatalf("unexpected ICMP response, got type %d, want = %d, %d or %d",
					replyICMPHeader.Type(), header.ICMPv4EchoReply, header.ICMPv4DstUnreachable, header.ICMPv4ParamProblem)
			}
		})
	}
}

// compareFragments compares the contents of a set of fragmented packets against
// the contents of a source packet.
//
// If withIPHeader is set to true, we will validate the fragmented packets' IP
// headers against the source packet's IP header. If set to false, we validate
// the fragmented packets' IP headers against each other.
func compareFragments(packets []*stack.PacketBuffer, sourcePacket *stack.PacketBuffer, mtu uint32, wantFragments []fragmentInfo, proto tcpip.TransportProtocolNumber, withIPHeader bool, expectedAvailableHeaderBytes int) error {
	// Make a complete array of the sourcePacket packet.
	var source header.IPv4
	buf := sourcePacket.ToBuffer()
	defer buf.Release()

	// If the packet to be fragmented contains an IPv4 header, use that header for
	// validating fragment headers. Else, use the header of the first fragment.
	if withIPHeader {
		source = header.IPv4(buf.Flatten())
	} else {
		source = header.IPv4(packets[0].NetworkHeader().Slice())
		source = append(source, buf.Flatten()...)
	}

	// Make a copy of the IP header, which will be modified in some fields to make
	// an expected header.
	sourceCopy := header.IPv4(append([]byte{}, source[:source.HeaderLength()]...))
	sourceCopy.SetChecksum(0)
	sourceCopy.SetFlagsFragmentOffset(0, 0)
	sourceCopy.SetTotalLength(0)
	// Build up an array of the bytes sent.
	var reassembledPayload buffer.Buffer
	defer reassembledPayload.Release()
	for i, packet := range packets {
		// Confirm that the packet is valid.
		allBytes := packet.ToBuffer()
		defer allBytes.Release()
		fragmentIPHeader := header.IPv4(allBytes.Flatten())
		if !fragmentIPHeader.IsValid(len(fragmentIPHeader)) {
			return fmt.Errorf("fragment #%d: IP packet is invalid:\n%s", i, hex.Dump(fragmentIPHeader))
		}
		if got := len(fragmentIPHeader); got > int(mtu) {
			return fmt.Errorf("fragment #%d: got len(fragmentIPHeader) = %d, want <= %d", i, got, mtu)
		}
		if got := fragmentIPHeader.TransportProtocol(); got != proto {
			return fmt.Errorf("fragment #%d: got fragmentIPHeader.TransportProtocol() = %d, want = %d", i, got, uint8(proto))
		}
		if got, want := packet.NetworkProtocolNumber, sourcePacket.NetworkProtocolNumber; got != want {
			return fmt.Errorf("fragment #%d: got fragment.NetworkProtocolNumber = %d, want = %d", i, got, want)
		}
		if got := packet.AvailableHeaderBytes(); got != expectedAvailableHeaderBytes {
			return fmt.Errorf("fragment #%d: got packet.AvailableHeaderBytes() = %d, want = %d", i, got, expectedAvailableHeaderBytes)
		}
		if got, want := fragmentIPHeader.CalculateChecksum(), uint16(0xffff); got != want {
			return fmt.Errorf("fragment #%d: got ip.CalculateChecksum() = %#x, want = %#x", i, got, want)
		}
		if wantFragments[i].more {
			sourceCopy.SetFlagsFragmentOffset(sourceCopy.Flags()|header.IPv4FlagMoreFragments, wantFragments[i].offset)
		} else {
			sourceCopy.SetFlagsFragmentOffset(sourceCopy.Flags()&^header.IPv4FlagMoreFragments, wantFragments[i].offset)
		}
		reassembledPayload.Append(packet.TransportHeader().View())
		reassembledPayload.Append(packet.Data().AsRange().ToView())
		// Clear out the checksum and length from the ip because we can't compare
		// it.
		sourceCopy.SetTotalLength(wantFragments[i].payloadSize + header.IPv4MinimumSize)
		sourceCopy.SetChecksum(0)
		sourceCopy.SetChecksum(^sourceCopy.CalculateChecksum())

		// If we are validating against the original IP header, we should exclude the
		// ID field, which will only be set fo fragmented packets.
		if withIPHeader {
			fragmentIPHeader.SetID(0)
			fragmentIPHeader.SetChecksum(0)
			fragmentIPHeader.SetChecksum(^fragmentIPHeader.CalculateChecksum())
		}
		if diff := cmp.Diff(fragmentIPHeader[:fragmentIPHeader.HeaderLength()], sourceCopy[:sourceCopy.HeaderLength()]); diff != "" {
			return fmt.Errorf("fragment #%d: fragmentIPHeader mismatch (-want +got):\n%s", i, diff)
		}
	}

	expected := []byte(source[source.HeaderLength():])
	if diff := cmp.Diff(expected, reassembledPayload.Flatten()); diff != "" {
		return fmt.Errorf("reassembledPayload mismatch (-want +got):\n%s", diff)
	}

	return nil
}

type fragmentInfo struct {
	offset      uint16
	more        bool
	payloadSize uint16
}

var fragmentationTests = []struct {
	description           string
	mtu                   uint32
	transportHeaderLength int
	payloadSize           int
	wantFragments         []fragmentInfo
}{
	{
		description:           "No fragmentation",
		mtu:                   1280,
		transportHeaderLength: 0,
		payloadSize:           1000,
		wantFragments: []fragmentInfo{
			{offset: 0, payloadSize: 1000, more: false},
		},
	},
	{
		description:           "Fragmented",
		mtu:                   1280,
		transportHeaderLength: 0,
		payloadSize:           2000,
		wantFragments: []fragmentInfo{
			{offset: 0, payloadSize: 1256, more: true},
			{offset: 1256, payloadSize: 744, more: false},
		},
	},
	{
		description:           "Fragmented with the minimum mtu",
		mtu:                   header.IPv4MinimumMTU,
		transportHeaderLength: 0,
		payloadSize:           100,
		wantFragments: []fragmentInfo{
			{offset: 0, payloadSize: 48, more: true},
			{offset: 48, payloadSize: 48, more: true},
			{offset: 96, payloadSize: 4, more: false},
		},
	},
	{
		description:           "Fragmented with mtu not a multiple of 8",
		mtu:                   header.IPv4MinimumMTU + 1,
		transportHeaderLength: 0,
		payloadSize:           100,
		wantFragments: []fragmentInfo{
			{offset: 0, payloadSize: 48, more: true},
			{offset: 48, payloadSize: 48, more: true},
			{offset: 96, payloadSize: 4, more: false},
		},
	},
	{
		description:           "No fragmentation with big header",
		mtu:                   2000,
		transportHeaderLength: 100,
		payloadSize:           1000,
		wantFragments: []fragmentInfo{
			{offset: 0, payloadSize: 1100, more: false},
		},
	},
	{
		description:           "Fragmented with big header",
		mtu:                   1280,
		transportHeaderLength: 100,
		payloadSize:           1200,
		wantFragments: []fragmentInfo{
			{offset: 0, payloadSize: 1256, more: true},
			{offset: 1256, payloadSize: 44, more: false},
		},
	},
	{
		description:           "Fragmented with MTU smaller than header",
		mtu:                   300,
		transportHeaderLength: 1000,
		payloadSize:           500,
		wantFragments: []fragmentInfo{
			{offset: 0, payloadSize: 280, more: true},
			{offset: 280, payloadSize: 280, more: true},
			{offset: 560, payloadSize: 280, more: true},
			{offset: 840, payloadSize: 280, more: true},
			{offset: 1120, payloadSize: 280, more: true},
			{offset: 1400, payloadSize: 100, more: false},
		},
	},
}

func TestFragmentationWritePacket(t *testing.T) {
	const ttl = 42

	for _, ft := range fragmentationTests {
		t.Run(ft.description, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()

			ep := iptestutil.NewMockLinkEndpoint(ft.mtu, nil, math.MaxInt32)
			defer ep.Close()
			r := buildRoute(t, ctx, ep)
			defer r.Release()
			pkt := iptestutil.MakeRandPkt(ft.transportHeaderLength, extraHeaderReserve+header.IPv4MinimumSize, []int{ft.payloadSize}, header.IPv4ProtocolNumber)
			defer pkt.DecRef()
			source := pkt.Clone()
			defer source.DecRef()
			err := r.WritePacket(stack.NetworkHeaderParams{
				Protocol: tcp.ProtocolNumber,
				TTL:      ttl,
				TOS:      stack.DefaultTOS,
			}, pkt)
			if err != nil {
				t.Fatalf("r.WritePacket(...): %s", err)
			}
			if got := len(ep.WrittenPackets); got != len(ft.wantFragments) {
				t.Errorf("got len(ep.WrittenPackets) = %d, want = %d", got, len(ft.wantFragments))
			}
			if got := int(r.Stats().IP.PacketsSent.Value()); got != len(ft.wantFragments) {
				t.Errorf("got c.Route.Stats().IP.PacketsSent.Value() = %d, want = %d", got, len(ft.wantFragments))
			}
			if got := r.Stats().IP.OutgoingPacketErrors.Value(); got != 0 {
				t.Errorf("got r.Stats().IP.OutgoingPacketErrors.Value() = %d, want = 0", got)
			}
			if err := compareFragments(ep.WrittenPackets, source, ft.mtu, ft.wantFragments, tcp.ProtocolNumber, false /* withIPHeader */, extraHeaderReserve); err != nil {
				t.Error(err)
			}
		})
	}
}

// TestFragmentationErrors checks that errors are returned from WritePacket
// correctly.
func TestFragmentationErrors(t *testing.T) {
	const ttl = 42

	tests := []struct {
		description           string
		mtu                   uint32
		transportHeaderLength int
		payloadSize           int
		allowPackets          int
		outgoingErrors        int
		mockError             tcpip.Error
		wantError             tcpip.Error
	}{
		{
			description:           "No frag",
			mtu:                   2000,
			payloadSize:           1000,
			transportHeaderLength: 0,
			allowPackets:          0,
			outgoingErrors:        1,
			mockError:             &tcpip.ErrAborted{},
			wantError:             &tcpip.ErrAborted{},
		},
		{
			description:           "Error on first frag",
			mtu:                   500,
			payloadSize:           1000,
			transportHeaderLength: 0,
			allowPackets:          0,
			outgoingErrors:        3,
			mockError:             &tcpip.ErrAborted{},
			wantError:             &tcpip.ErrAborted{},
		},
		{
			description:           "Error on second frag",
			mtu:                   500,
			payloadSize:           1000,
			transportHeaderLength: 0,
			allowPackets:          1,
			outgoingErrors:        2,
			mockError:             &tcpip.ErrAborted{},
			wantError:             &tcpip.ErrAborted{},
		},
		{
			description:           "Error on first frag MTU smaller than header",
			mtu:                   500,
			transportHeaderLength: 1000,
			payloadSize:           500,
			allowPackets:          0,
			outgoingErrors:        4,
			mockError:             &tcpip.ErrAborted{},
			wantError:             &tcpip.ErrAborted{},
		},
		{
			description:           "Error when MTU is smaller than IPv4 minimum MTU",
			mtu:                   header.IPv4MinimumMTU - 1,
			transportHeaderLength: 0,
			payloadSize:           500,
			allowPackets:          0,
			outgoingErrors:        1,
			mockError:             nil,
			wantError:             &tcpip.ErrInvalidEndpointState{},
		},
	}

	for _, ft := range tests {
		t.Run(ft.description, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()

			ep := iptestutil.NewMockLinkEndpoint(ft.mtu, ft.mockError, ft.allowPackets)
			defer ep.Close()
			r := buildRoute(t, ctx, ep)
			defer r.Release()
			pkt := iptestutil.MakeRandPkt(ft.transportHeaderLength, extraHeaderReserve+header.IPv4MinimumSize, []int{ft.payloadSize}, header.IPv4ProtocolNumber)
			defer pkt.DecRef()
			err := r.WritePacket(stack.NetworkHeaderParams{
				Protocol: tcp.ProtocolNumber,
				TTL:      ttl,
				TOS:      stack.DefaultTOS,
			}, pkt)
			if diff := cmp.Diff(ft.wantError, err); diff != "" {
				t.Fatalf("unexpected error from r.WritePacket(_, _, _), (-want, +got):\n%s", diff)
			}
			if got := int(r.Stats().IP.PacketsSent.Value()); got != ft.allowPackets {
				t.Errorf("got r.Stats().IP.PacketsSent.Value() = %d, want = %d", got, ft.allowPackets)
			}
			if got := int(r.Stats().IP.OutgoingPacketErrors.Value()); got != ft.outgoingErrors {
				t.Errorf("got r.Stats().IP.OutgoingPacketErrors.Value() = %d, want = %d", got, ft.outgoingErrors)
			}
		})
	}
}

func TestInvalidFragments(t *testing.T) {
	const (
		nicID    = 1
		linkAddr = tcpip.LinkAddress("\x0a\x0b\x0c\x0d\x0e\x0e")
		tos      = 0
		ident    = 1
		ttl      = 48
		protocol = 6
	)

	var (
		addr1 = tcpip.AddrFromSlice([]byte("\x0a\x00\x00\x01"))
		addr2 = tcpip.AddrFromSlice([]byte("\x0a\x00\x00\x02"))
	)

	payloadGen := func(payloadLen int) []byte {
		payload := make([]byte, payloadLen)
		for i := 0; i < len(payload); i++ {
			payload[i] = 0x30
		}
		return payload
	}

	type fragmentData struct {
		ipv4fields header.IPv4Fields
		// 0 means insert the correct IHL. Non 0 means override the correct IHL.
		overrideIHL  int // For 0 use 1 as it is an int and will be divided by 4.
		payload      []byte
		autoChecksum bool // If true, the Checksum field will be overwritten.
	}

	tests := []struct {
		name                   string
		fragments              []fragmentData
		wantMalformedIPPackets uint64
		wantMalformedFragments uint64
	}{
		{
			name: "IHL and TotalLength zero, FragmentOffset non-zero",
			fragments: []fragmentData{
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    0,
						ID:             ident,
						Flags:          header.IPv4FlagDontFragment | header.IPv4FlagMoreFragments,
						FragmentOffset: 59776,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					overrideIHL:  1, // See note above.
					payload:      payloadGen(12),
					autoChecksum: true,
				},
			},
			wantMalformedIPPackets: 1,
			wantMalformedFragments: 0,
		},
		{
			name: "IHL and TotalLength zero, FragmentOffset zero",
			fragments: []fragmentData{
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    0,
						ID:             ident,
						Flags:          header.IPv4FlagMoreFragments,
						FragmentOffset: 0,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					overrideIHL:  1, // See note above.
					payload:      payloadGen(12),
					autoChecksum: true,
				},
			},
			wantMalformedIPPackets: 1,
			wantMalformedFragments: 0,
		},
		{
			// Payload 17 octets and Fragment offset 65520
			// Leading to the fragment end to be past 65536.
			name: "fragment ends past 65536",
			fragments: []fragmentData{
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 17,
						ID:             ident,
						Flags:          0,
						FragmentOffset: 65520,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload:      payloadGen(17),
					autoChecksum: true,
				},
			},
			wantMalformedIPPackets: 1,
			wantMalformedFragments: 1,
		},
		{
			// Payload 16 octets and fragment offset 65520
			// Leading to the fragment end to be exactly 65536.
			name: "fragment ends exactly at 65536",
			fragments: []fragmentData{
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 16,
						ID:             ident,
						Flags:          0,
						FragmentOffset: 65520,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload:      payloadGen(16),
					autoChecksum: true,
				},
			},
			wantMalformedIPPackets: 0,
			wantMalformedFragments: 0,
		},
		{
			name: "IHL less than IPv4 minimum size",
			fragments: []fragmentData{
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 28,
						ID:             ident,
						Flags:          0,
						FragmentOffset: 1944,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload:      payloadGen(28),
					overrideIHL:  header.IPv4MinimumSize - 12,
					autoChecksum: true,
				},
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize - 12,
						ID:             ident,
						Flags:          header.IPv4FlagMoreFragments,
						FragmentOffset: 0,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload:      payloadGen(28),
					overrideIHL:  header.IPv4MinimumSize - 12,
					autoChecksum: true,
				},
			},
			wantMalformedIPPackets: 2,
			wantMalformedFragments: 0,
		},
		{
			name: "fragment with short TotalLength and extra payload",
			fragments: []fragmentData{
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 28,
						ID:             ident,
						Flags:          0,
						FragmentOffset: 28816,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload:      payloadGen(28),
					overrideIHL:  header.IPv4MinimumSize + 4,
					autoChecksum: true,
				},
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 4,
						ID:             ident,
						Flags:          header.IPv4FlagMoreFragments,
						FragmentOffset: 0,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload:      payloadGen(28),
					overrideIHL:  header.IPv4MinimumSize + 4,
					autoChecksum: true,
				},
			},
			wantMalformedIPPackets: 1,
			wantMalformedFragments: 1,
		},
		{
			name: "multiple fragments with More Fragments flag set to false",
			fragments: []fragmentData{
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 8,
						ID:             ident,
						Flags:          0,
						FragmentOffset: 128,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload:      payloadGen(8),
					autoChecksum: true,
				},
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 8,
						ID:             ident,
						Flags:          0,
						FragmentOffset: 8,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload:      payloadGen(8),
					autoChecksum: true,
				},
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 8,
						ID:             ident,
						Flags:          header.IPv4FlagMoreFragments,
						FragmentOffset: 0,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload:      payloadGen(8),
					autoChecksum: true,
				},
			},
			wantMalformedIPPackets: 1,
			wantMalformedFragments: 1,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()
			s := ctx.s

			e := channel.New(0, 1500, linkAddr)
			defer e.Close()
			if err := s.CreateNIC(nicID, e); err != nil {
				t.Fatalf("CreateNIC(%d, _) = %s", nicID, err)
			}
			protocolAddr := tcpip.ProtocolAddress{
				Protocol:          ipv4.ProtocolNumber,
				AddressWithPrefix: addr2.WithPrefix(),
			}
			if err := s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil {
				t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, protocolAddr, err)
			}

			for _, f := range test.fragments {
				pktSize := header.IPv4MinimumSize + len(f.payload)
				hdr := prependable.New(pktSize)

				ip := header.IPv4(hdr.Prepend(pktSize))
				ip.Encode(&f.ipv4fields)
				if want, got := len(f.payload), copy(ip[header.IPv4MinimumSize:], f.payload); want != got {
					t.Fatalf("copied %d bytes, expected %d bytes.", got, want)
				}
				// Encode sets this up correctly. If we want a different value for
				// testing then we need to overwrite the good value.
				if f.overrideIHL != 0 {
					ip.SetHeaderLength(uint8(f.overrideIHL))
					// If we are asked to add options (type not specified) then pad
					// with 0 (EOL). RFC 791 page 23 says "The padding is zero".
					for i := header.IPv4MinimumSize; i < f.overrideIHL; i++ {
						ip[i] = byte(header.IPv4OptionListEndType)
					}
				}

				if f.autoChecksum {
					ip.SetChecksum(0)
					ip.SetChecksum(^ip.CalculateChecksum())
				}

				pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
					Payload: buffer.MakeWithData(hdr.View()),
				})
				e.InjectInbound(header.IPv4ProtocolNumber, pkt)
				pkt.DecRef()
			}

			if got, want := s.Stats().IP.MalformedPacketsReceived.Value(), test.wantMalformedIPPackets; got != want {
				t.Errorf("incorrect Stats.IP.MalformedPacketsReceived, got: %d, want: %d", got, want)
			}
			if got, want := s.Stats().IP.MalformedFragmentsReceived.Value(), test.wantMalformedFragments; got != want {
				t.Errorf("incorrect Stats.IP.MalformedFragmentsReceived, got: %d, want: %d", got, want)
			}
		})
	}
}

func TestFragmentReassemblyTimeout(t *testing.T) {
	const (
		nicID    = 1
		linkAddr = tcpip.LinkAddress("\x0a\x0b\x0c\x0d\x0e\x0e")
		tos      = 0
		ident    = 1
		ttl      = 48
		protocol = 99
		data     = "TEST_FRAGMENT_REASSEMBLY_TIMEOUT"
	)

	var (
		addr1 = tcpip.AddrFromSlice([]byte("\x0a\x00\x00\x01"))
		addr2 = tcpip.AddrFromSlice([]byte("\x0a\x00\x00\x02"))
	)

	type fragmentData struct {
		ipv4fields header.IPv4Fields
		payload    []byte
	}

	tests := []struct {
		name       string
		fragments  []fragmentData
		expectICMP bool
	}{
		{
			name: "first fragment only",
			fragments: []fragmentData{
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 16,
						ID:             ident,
						Flags:          header.IPv4FlagMoreFragments,
						FragmentOffset: 0,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload: []byte(data)[:16],
				},
			},
			expectICMP: true,
		},
		{
			name: "two first fragments",
			fragments: []fragmentData{
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 16,
						ID:             ident,
						Flags:          header.IPv4FlagMoreFragments,
						FragmentOffset: 0,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload: []byte(data)[:16],
				},
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 16,
						ID:             ident,
						Flags:          header.IPv4FlagMoreFragments,
						FragmentOffset: 0,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload: []byte(data)[:16],
				},
			},
			expectICMP: true,
		},
		{
			name: "second fragment only",
			fragments: []fragmentData{
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    uint16(header.IPv4MinimumSize + len(data) - 16),
						ID:             ident,
						Flags:          0,
						FragmentOffset: 8,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload: []byte(data)[16:],
				},
			},
			expectICMP: false,
		},
		{
			name: "two fragments with a gap",
			fragments: []fragmentData{
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 8,
						ID:             ident,
						Flags:          header.IPv4FlagMoreFragments,
						FragmentOffset: 0,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload: []byte(data)[:8],
				},
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    uint16(header.IPv4MinimumSize + len(data) - 16),
						ID:             ident,
						Flags:          0,
						FragmentOffset: 16,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload: []byte(data)[16:],
				},
			},
			expectICMP: true,
		},
		{
			name: "two fragments with a gap in reverse order",
			fragments: []fragmentData{
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    uint16(header.IPv4MinimumSize + len(data) - 16),
						ID:             ident,
						Flags:          0,
						FragmentOffset: 16,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload: []byte(data)[16:],
				},
				{
					ipv4fields: header.IPv4Fields{
						TOS:            tos,
						TotalLength:    header.IPv4MinimumSize + 8,
						ID:             ident,
						Flags:          header.IPv4FlagMoreFragments,
						FragmentOffset: 0,
						TTL:            ttl,
						Protocol:       protocol,
						SrcAddr:        addr1,
						DstAddr:        addr2,
					},
					payload: []byte(data)[:8],
				},
			},
			expectICMP: true,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()
			s := ctx.s
			clock := ctx.clock

			e := channel.New(1, 1500, linkAddr)
			defer e.Close()
			if err := s.CreateNIC(nicID, e); err != nil {
				t.Fatalf("CreateNIC(%d, _) = %s", nicID, err)
			}
			protocolAddr := tcpip.ProtocolAddress{
				Protocol:          ipv4.ProtocolNumber,
				AddressWithPrefix: addr2.WithPrefix(),
			}
			if err := s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil {
				t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, protocolAddr, err)
			}
			s.SetRouteTable([]tcpip.Route{{
				Destination: header.IPv4EmptySubnet,
				NIC:         nicID,
			}})

			var firstFragmentSent buffer.Buffer
			for _, f := range test.fragments {
				pktSize := header.IPv4MinimumSize
				hdr := prependable.New(pktSize)

				ip := header.IPv4(hdr.Prepend(pktSize))
				ip.Encode(&f.ipv4fields)

				ip.SetChecksum(0)
				ip.SetChecksum(^ip.CalculateChecksum())

				buf := buffer.MakeWithData(hdr.View())
				buf.Append(buffer.NewViewWithData(f.payload))

				pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
					Payload: buf,
				})

				if firstFragmentSent.Size() == 0 && ip.FragmentOffset() == 0 {
					firstFragmentSent = buffer.MakeWithView(stack.PayloadSince(pkt.NetworkHeader()))
					defer firstFragmentSent.Release()
				}

				e.InjectInbound(header.IPv4ProtocolNumber, pkt)
				pkt.DecRef()
			}

			clock.Advance(ipv4.ReassembleTimeout)

			reply := e.Read()
			if !test.expectICMP {
				if reply != nil {
					t.Fatalf("unexpected ICMP error message received: %#v", reply)
				}
				return
			}
			if reply == nil {
				t.Fatal("expected ICMP error message missing")
			}
			if firstFragmentSent.Size() == 0 {
				t.Fatalf("unexpected ICMP error message received: %#v", reply)
			}

			payload := stack.PayloadSince(reply.NetworkHeader())
			defer payload.Release()
			checker.IPv4(t, payload,
				checker.SrcAddr(addr2),
				checker.DstAddr(addr1),
				checker.IPFullLength(uint16(header.IPv4MinimumSize+header.ICMPv4MinimumSize+firstFragmentSent.Size())),
				checker.IPv4HeaderLength(header.IPv4MinimumSize),
				checker.ICMPv4(
					checker.ICMPv4Type(header.ICMPv4TimeExceeded),
					checker.ICMPv4Code(header.ICMPv4ReassemblyTimeout),
					checker.ICMPv4Checksum(),
					checker.ICMPv4Payload(firstFragmentSent.Flatten()),
				),
			)
			reply.DecRef()
		})
	}
}

// TestReceiveFragments feeds fragments in through the incoming packet path to
// test reassembly
func TestReceiveFragments(t *testing.T) {
	const (
		nicID = 1
	)

	var (
		addr1 = tcpip.AddrFromSlice([]byte("\x0c\xa8\x00\x01")) // 192.168.0.1
		addr2 = tcpip.AddrFromSlice([]byte("\x0c\xa8\x00\x02")) // 192.168.0.2
		addr3 = tcpip.AddrFromSlice([]byte("\x0c\xa8\x00\x03")) // 192.168.0.3
	)

	// Build and return a UDP header containing payload.
	udpGen := func(payloadLen int, multiplier uint8, src, dst tcpip.Address) []byte {
		payload := make([]byte, payloadLen)
		for i := 0; i < len(payload); i++ {
			payload[i] = uint8(i) * multiplier
		}

		udpLength := header.UDPMinimumSize + len(payload)

		hdr := prependable.New(udpLength)
		u := header.UDP(hdr.Prepend(udpLength))
		u.Encode(&header.UDPFields{
			SrcPort: 5555,
			DstPort: 80,
			Length:  uint16(udpLength),
		})
		copy(u.Payload(), payload)
		sum := header.PseudoHeaderChecksum(udp.ProtocolNumber, src, dst, uint16(udpLength))
		sum = checksum.Checksum(payload, sum)
		u.SetChecksum(^u.CalculateChecksum(sum))
		return hdr.View()
	}

	// UDP header plus a payload of 0..256
	ipv4Payload1Addr1ToAddr2 := udpGen(256, 1, addr1, addr2)
	udpPayload1Addr1ToAddr2 := ipv4Payload1Addr1ToAddr2[header.UDPMinimumSize:]
	ipv4Payload1Addr3ToAddr2 := udpGen(256, 1, addr3, addr2)
	udpPayload1Addr3ToAddr2 := ipv4Payload1Addr3ToAddr2[header.UDPMinimumSize:]
	// UDP header plus a payload of 0..256 in increments of 2.
	ipv4Payload2Addr1ToAddr2 := udpGen(128, 2, addr1, addr2)
	udpPayload2Addr1ToAddr2 := ipv4Payload2Addr1ToAddr2[header.UDPMinimumSize:]
	// UDP header plus a payload of 0..256 in increments of 3.
	// Used to test cases where the fragment blocks are not a multiple of
	// the fragment block size of 8 (RFC 791 section 3.1 page 14).
	ipv4Payload3Addr1ToAddr2 := udpGen(127, 3, addr1, addr2)
	udpPayload3Addr1ToAddr2 := ipv4Payload3Addr1ToAddr2[header.UDPMinimumSize:]
	// Used to test the max reassembled IPv4 payload length.
	ipv4Payload4Addr1ToAddr2 := udpGen(header.UDPMaximumSize-header.UDPMinimumSize, 4, addr1, addr2)
	udpPayload4Addr1ToAddr2 := ipv4Payload4Addr1ToAddr2[header.UDPMinimumSize:]

	type fragmentData struct {
		srcAddr        tcpip.Address
		dstAddr        tcpip.Address
		id             uint16
		flags          uint8
		fragmentOffset uint16
		payload        []byte
	}

	tests := []struct {
		name             string
		fragments        []fragmentData
		expectedPayloads [][]byte
	}{
		{
			name: "No fragmentation",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          0,
					fragmentOffset: 0,
					payload:        ipv4Payload1Addr1ToAddr2,
				},
			},
			expectedPayloads: [][]byte{udpPayload1Addr1ToAddr2},
		},
		{
			name: "No fragmentation with size not a multiple of fragment block size",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          0,
					fragmentOffset: 0,
					payload:        ipv4Payload3Addr1ToAddr2,
				},
			},
			expectedPayloads: [][]byte{udpPayload3Addr1ToAddr2},
		},
		{
			name: "More fragments without payload",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload1Addr1ToAddr2,
				},
			},
			expectedPayloads: nil,
		},
		{
			name: "Non-zero fragment offset without payload",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          0,
					fragmentOffset: 8,
					payload:        ipv4Payload1Addr1ToAddr2,
				},
			},
			expectedPayloads: nil,
		},
		{
			name: "Two fragments",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload1Addr1ToAddr2[:64],
				},
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          0,
					fragmentOffset: 64,
					payload:        ipv4Payload1Addr1ToAddr2[64:],
				},
			},
			expectedPayloads: [][]byte{udpPayload1Addr1ToAddr2},
		},
		{
			name: "Two fragments out of order",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          0,
					fragmentOffset: 64,
					payload:        ipv4Payload1Addr1ToAddr2[64:],
				},
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload1Addr1ToAddr2[:64],
				},
			},
			expectedPayloads: [][]byte{udpPayload1Addr1ToAddr2},
		},
		{
			name: "Two fragments with last fragment size not a multiple of fragment block size",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload3Addr1ToAddr2[:64],
				},
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          0,
					fragmentOffset: 64,
					payload:        ipv4Payload3Addr1ToAddr2[64:],
				},
			},
			expectedPayloads: [][]byte{udpPayload3Addr1ToAddr2},
		},
		{
			name: "Two fragments with first fragment size not a multiple of fragment block size",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload3Addr1ToAddr2[:63],
				},
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          0,
					fragmentOffset: 63,
					payload:        ipv4Payload3Addr1ToAddr2[63:],
				},
			},
			expectedPayloads: nil,
		},
		{
			name: "Second fragment has MoreFlags set",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload1Addr1ToAddr2[:64],
				},
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 64,
					payload:        ipv4Payload1Addr1ToAddr2[64:],
				},
			},
			expectedPayloads: nil,
		},
		{
			name: "Two fragments with different IDs",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload1Addr1ToAddr2[:64],
				},
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             2,
					flags:          0,
					fragmentOffset: 64,
					payload:        ipv4Payload1Addr1ToAddr2[64:],
				},
			},
			expectedPayloads: nil,
		},
		{
			name: "Two interleaved fragmented packets",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload1Addr1ToAddr2[:64],
				},
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             2,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload2Addr1ToAddr2[:64],
				},
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          0,
					fragmentOffset: 64,
					payload:        ipv4Payload1Addr1ToAddr2[64:],
				},
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             2,
					flags:          0,
					fragmentOffset: 64,
					payload:        ipv4Payload2Addr1ToAddr2[64:],
				},
			},
			expectedPayloads: [][]byte{udpPayload1Addr1ToAddr2, udpPayload2Addr1ToAddr2},
		},
		{
			name: "Two interleaved fragmented packets from different sources but with same ID",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload1Addr1ToAddr2[:64],
				},
				{
					srcAddr:        addr3,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload1Addr3ToAddr2[:32],
				},
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          0,
					fragmentOffset: 64,
					payload:        ipv4Payload1Addr1ToAddr2[64:],
				},
				{
					srcAddr:        addr3,
					dstAddr:        addr2,
					id:             1,
					flags:          0,
					fragmentOffset: 32,
					payload:        ipv4Payload1Addr3ToAddr2[32:],
				},
			},
			expectedPayloads: [][]byte{udpPayload1Addr1ToAddr2, udpPayload1Addr3ToAddr2},
		},
		{
			name: "Fragment without followup",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload1Addr1ToAddr2[:64],
				},
			},
			expectedPayloads: nil,
		},
		{
			name: "Two fragments reassembled into a maximum UDP packet",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload4Addr1ToAddr2[:65512],
				},
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          0,
					fragmentOffset: 65512,
					payload:        ipv4Payload4Addr1ToAddr2[65512:],
				},
			},
			expectedPayloads: [][]byte{udpPayload4Addr1ToAddr2},
		},
		{
			name: "Two fragments with MF flag reassembled into a maximum UDP packet",
			fragments: []fragmentData{
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 0,
					payload:        ipv4Payload4Addr1ToAddr2[:65512],
				},
				{
					srcAddr:        addr1,
					dstAddr:        addr2,
					id:             1,
					flags:          header.IPv4FlagMoreFragments,
					fragmentOffset: 65512,
					payload:        ipv4Payload4Addr1ToAddr2[65512:],
				},
			},
			expectedPayloads: nil,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()
			s := ctx.s

			e := channel.New(0, 1280, "\xf0\x00")
			defer e.Close()
			if err := s.CreateNIC(nicID, e); err != nil {
				t.Fatalf("CreateNIC(%d, _) = %s", nicID, err)
			}
			protocolAddr := tcpip.ProtocolAddress{
				Protocol:          header.IPv4ProtocolNumber,
				AddressWithPrefix: addr2.WithPrefix(),
			}
			if err := s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil {
				t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, protocolAddr, err)
			}

			wq := waiter.Queue{}
			we, ch := waiter.NewChannelEntry(waiter.ReadableEvents)
			wq.EventRegister(&we)
			defer wq.EventUnregister(&we)
			defer close(ch)
			ep, err := s.NewEndpoint(udp.ProtocolNumber, header.IPv4ProtocolNumber, &wq)
			if err != nil {
				t.Fatalf("NewEndpoint(%d, %d, _): %s", udp.ProtocolNumber, header.IPv4ProtocolNumber, err)
			}
			defer ep.Close()

			bindAddr := tcpip.FullAddress{Addr: addr2, Port: 80}
			if err := ep.Bind(bindAddr); err != nil {
				t.Fatalf("Bind(%+v): %s", bindAddr, err)
			}

			// Bring up a raw endpoint so we can examine network headers.
			epRaw, err := s.NewRawEndpoint(udp.ProtocolNumber, header.IPv4ProtocolNumber, &wq, true /* associated */)
			if err != nil {
				t.Fatalf("NewRawEndpoint(%d, %d, _, true): %s", udp.ProtocolNumber, header.IPv4ProtocolNumber, err)
			}
			defer epRaw.Close()

			// Prepare and send the fragments.
			for _, frag := range test.fragments {
				hdr := prependable.New(header.IPv4MinimumSize)

				// Serialize IPv4 fixed header.
				ip := header.IPv4(hdr.Prepend(header.IPv4MinimumSize))
				ip.Encode(&header.IPv4Fields{
					TotalLength:    header.IPv4MinimumSize + uint16(len(frag.payload)),
					ID:             frag.id,
					Flags:          frag.flags,
					FragmentOffset: frag.fragmentOffset,
					TTL:            64,
					Protocol:       uint8(header.UDPProtocolNumber),
					SrcAddr:        frag.srcAddr,
					DstAddr:        frag.dstAddr,
				})
				ip.SetChecksum(^ip.CalculateChecksum())

				buf := buffer.MakeWithData(hdr.View())
				buf.Append(buffer.NewViewWithData(frag.payload))
				pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
					Payload: buf,
				})
				e.InjectInbound(header.IPv4ProtocolNumber, pkt)
				pkt.DecRef()
			}

			if got, want := s.Stats().UDP.PacketsReceived.Value(), uint64(len(test.expectedPayloads)); got != want {
				t.Errorf("got UDP Rx Packets = %d, want = %d", got, want)
			}

			for i, expectedPayload := range test.expectedPayloads {
				// Check UDP payload delivered by UDP endpoint.
				var buf bytes.Buffer
				result, err := ep.Read(&buf, tcpip.ReadOptions{})
				if err != nil {
					t.Fatalf("(i=%d) ep.Read: %s", i, err)
				}
				if diff := cmp.Diff(tcpip.ReadResult{
					Count: len(expectedPayload),
					Total: len(expectedPayload),
				}, result, checker.IgnoreCmpPath("ControlMessages")); diff != "" {
					t.Errorf("(i=%d) ep.Read: unexpected result (-want +got):\n%s", i, diff)
				}
				if diff := cmp.Diff(expectedPayload, buf.Bytes()); diff != "" {
					t.Errorf("(i=%d) ep.Read: UDP payload mismatch (-want +got):\n%s", i, diff)
				}

				// Check IPv4 header in packet delivered by raw endpoint.
				buf.Reset()
				_, err = epRaw.Read(&buf, tcpip.ReadOptions{})
				if err != nil {
					t.Fatalf("(i=%d) epRaw.Read: %s", i, err)
				}
				// Reassambly does not take care of checksum. Here we write our own
				// check routine instead of using checker.IPv4.
				ip := header.IPv4(buf.Bytes())
				for _, check := range []checker.NetworkChecker{
					checker.FragmentFlags(0),
					checker.FragmentOffset(0),
					checker.IPFullLength(uint16(header.IPv4MinimumSize + header.UDPMinimumSize + len(expectedPayload))),
				} {
					check(t, []header.Network{ip})
				}
			}

			res, err := ep.Read(io.Discard, tcpip.ReadOptions{})
			if _, ok := err.(*tcpip.ErrWouldBlock); !ok {
				t.Fatalf("(last) got Read = (%#v, %v), want = (_, %s)", res, err, &tcpip.ErrWouldBlock{})
			}
		})
	}
}

func TestWriteStats(t *testing.T) {
	const nPackets = 3

	tests := []struct {
		name                     string
		setup                    func(*testing.T, *stack.Stack)
		allowPackets             int
		expectSent               int
		expectOutputDropped      int
		expectPostroutingDropped int
		expectWritten            int
	}{
		{
			name: "Accept all",
			// No setup needed, tables accept everything by default.
			setup:                    func(*testing.T, *stack.Stack) {},
			allowPackets:             math.MaxInt32,
			expectSent:               nPackets,
			expectOutputDropped:      0,
			expectPostroutingDropped: 0,
			expectWritten:            nPackets,
		}, {
			name: "Accept all with error",
			// No setup needed, tables accept everything by default.
			setup:                    func(*testing.T, *stack.Stack) {},
			allowPackets:             nPackets - 1,
			expectSent:               nPackets - 1,
			expectOutputDropped:      0,
			expectPostroutingDropped: 0,
			expectWritten:            nPackets - 1,
		}, {
			name: "Drop all with Output chain",
			setup: func(t *testing.T, stk *stack.Stack) {
				// Install Output DROP rule.
				ipt := stk.IPTables()
				filter := ipt.GetTable(stack.FilterID, false /* ipv6 */)
				ruleIdx := filter.BuiltinChains[stack.Output]
				filter.Rules[ruleIdx].Target = &stack.DropTarget{}
				ipt.ForceReplaceTable(stack.FilterID, filter, false /* ipv6 */)
			},
			allowPackets:             math.MaxInt32,
			expectSent:               0,
			expectOutputDropped:      nPackets,
			expectPostroutingDropped: 0,
			expectWritten:            nPackets,
		}, {
			name: "Drop all with Postrouting chain",
			setup: func(t *testing.T, stk *stack.Stack) {
				ipt := stk.IPTables()
				filter := ipt.GetTable(stack.NATID, false /* ipv6 */)
				ruleIdx := filter.BuiltinChains[stack.Postrouting]
				filter.Rules[ruleIdx].Target = &stack.DropTarget{}
				ipt.ForceReplaceTable(stack.NATID, filter, false /* ipv6 */)
			},
			allowPackets:             math.MaxInt32,
			expectSent:               0,
			expectOutputDropped:      0,
			expectPostroutingDropped: nPackets,
			expectWritten:            nPackets,
		}, {
			name: "Drop some with Output chain",
			setup: func(t *testing.T, stk *stack.Stack) {
				// Install Output DROP rule that matches only 1
				// of the 3 packets.
				ipt := stk.IPTables()
				filter := ipt.GetTable(stack.FilterID, false /* ipv6 */)
				// We'll match and DROP the last packet.
				ruleIdx := filter.BuiltinChains[stack.Output]
				filter.Rules[ruleIdx].Target = &stack.DropTarget{}
				filter.Rules[ruleIdx].Matchers = []stack.Matcher{&limitedMatcher{nPackets - 1}}
				// Make sure the next rule is ACCEPT.
				filter.Rules[ruleIdx+1].Target = &stack.AcceptTarget{}
				ipt.ForceReplaceTable(stack.FilterID, filter, false /* ipv6 */)
			},
			allowPackets:             math.MaxInt32,
			expectSent:               nPackets - 1,
			expectOutputDropped:      1,
			expectPostroutingDropped: 0,
			expectWritten:            nPackets,
		}, {
			name: "Drop some with Postrouting chain",
			setup: func(t *testing.T, stk *stack.Stack) {
				// Install Postrouting DROP rule that matches only 1
				// of the 3 packets.
				ipt := stk.IPTables()
				filter := ipt.GetTable(stack.NATID, false /* ipv6 */)
				// We'll match and DROP the last packet.
				ruleIdx := filter.BuiltinChains[stack.Postrouting]
				filter.Rules[ruleIdx].Target = &stack.DropTarget{}
				filter.Rules[ruleIdx].Matchers = []stack.Matcher{&limitedMatcher{nPackets - 1}}
				// Make sure the next rule is ACCEPT.
				filter.Rules[ruleIdx+1].Target = &stack.AcceptTarget{}
				ipt.ForceReplaceTable(stack.NATID, filter, false /* ipv6 */)
			},
			allowPackets:             math.MaxInt32,
			expectSent:               nPackets - 1,
			expectOutputDropped:      0,
			expectPostroutingDropped: 1,
			expectWritten:            nPackets,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()

			ep := iptestutil.NewMockLinkEndpoint(header.IPv4MinimumMTU, &tcpip.ErrInvalidEndpointState{}, test.allowPackets)
			defer ep.Close()
			rt := buildRoute(t, ctx, ep)
			defer rt.Release()

			test.setup(t, rt.Stack())
			nWritten := 0
			for i := 0; i < nPackets; i++ {
				pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
					ReserveHeaderBytes: header.UDPMinimumSize + int(rt.MaxHeaderLength()),
					Payload:            buffer.Buffer{},
				})
				defer pkt.DecRef()
				pkt.TransportHeader().Push(header.UDPMinimumSize)
				if err := rt.WritePacket(stack.NetworkHeaderParams{}, pkt); err != nil {
					break
				}
				nWritten++
			}

			if got := int(rt.Stats().IP.PacketsSent.Value()); got != test.expectSent {
				t.Errorf("got rt.Stats().IP.PacketsSent.Value() = %d, want = %d", got, test.expectSent)
			}
			if got := int(rt.Stats().IP.IPTablesOutputDropped.Value()); got != test.expectOutputDropped {
				t.Errorf("got rt.Stats().IP.IPTablesOutputDropped.Value() = %d, want = %d", got, test.expectOutputDropped)
			}
			if got := int(rt.Stats().IP.IPTablesPostroutingDropped.Value()); got != test.expectPostroutingDropped {
				t.Errorf("got rt.Stats().IP.IPTablesPostroutingDropped.Value() = %d, want = %d", got, test.expectPostroutingDropped)
			}
			if nWritten != test.expectWritten {
				t.Errorf("got nWritten = %d, want = %d", nWritten, test.expectWritten)
			}
		})
	}
}

func buildRoute(t *testing.T, c testContext, ep stack.LinkEndpoint) *stack.Route {
	s := c.s
	if err := s.CreateNIC(1, ep); err != nil {
		t.Fatalf("CreateNIC(1, _) failed: %s", err)
	}
	var (
		src = tcpip.AddrFromSlice([]byte("\x10\x00\x00\x01"))
		dst = tcpip.AddrFromSlice([]byte("\x10\x00\x00\x02"))
	)
	protocolAddr := tcpip.ProtocolAddress{
		Protocol:          ipv4.ProtocolNumber,
		AddressWithPrefix: src.WithPrefix(),
	}
	if err := s.AddProtocolAddress(1, protocolAddr, stack.AddressProperties{}); err != nil {
		t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", 1, protocolAddr, err)
	}
	{
		mask := tcpip.MaskFromBytes(header.IPv4Broadcast.AsSlice())
		subnet, err := tcpip.NewSubnet(dst, mask)
		if err != nil {
			t.Fatalf("NewSubnet(%s, %s) failed: %v", dst, mask, err)
		}
		s.SetRouteTable([]tcpip.Route{{
			Destination: subnet,
			NIC:         1,
		}})
	}
	rt, err := s.FindRoute(1, src, dst, ipv4.ProtocolNumber, false /* multicastLoop */)
	if err != nil {
		t.Fatalf("FindRoute(1, %s, %s, %d, false) = %s", src, dst, ipv4.ProtocolNumber, err)
	}
	return rt
}

// limitedMatcher is an iptables matcher that matches after a certain number of
// packets are checked against it.
type limitedMatcher struct {
	limit int
}

// Name implements Matcher.Name.
func (*limitedMatcher) Name() string {
	return "limitedMatcher"
}

// Match implements Matcher.Match.
func (lm *limitedMatcher) Match(stack.Hook, *stack.PacketBuffer, string, string) (bool, bool) {
	if lm.limit == 0 {
		return true, false
	}
	lm.limit--
	return false, false
}

func TestPacketQueuing(t *testing.T) {
	const nicID = 1

	var (
		host1NICLinkAddr = tcpip.LinkAddress("\x02\x03\x03\x04\x05\x06")
		host2NICLinkAddr = tcpip.LinkAddress("\x02\x03\x03\x04\x05\x09")

		host1IPv4Addr = tcpip.ProtocolAddress{
			Protocol: ipv4.ProtocolNumber,
			AddressWithPrefix: tcpip.AddressWithPrefix{
				Address:   tcpip.AddrFromSlice(net.ParseIP("192.168.0.1").To4()),
				PrefixLen: 24,
			},
		}
		host2IPv4Addr = tcpip.ProtocolAddress{
			Protocol: ipv4.ProtocolNumber,
			AddressWithPrefix: tcpip.AddressWithPrefix{
				Address:   tcpip.AddrFromSlice(net.ParseIP("192.168.0.2").To4()),
				PrefixLen: 8,
			},
		}
	)

	tests := []struct {
		name      string
		rxPkt     func(*channel.Endpoint)
		checkResp func(*testing.T, *channel.Endpoint)
	}{
		{
			name: "ICMP Error",
			rxPkt: func(e *channel.Endpoint) {
				hdr := prependable.New(header.IPv4MinimumSize + header.UDPMinimumSize)
				u := header.UDP(hdr.Prepend(header.UDPMinimumSize))
				u.Encode(&header.UDPFields{
					SrcPort: 5555,
					DstPort: 80,
					Length:  header.UDPMinimumSize,
				})
				sum := header.PseudoHeaderChecksum(udp.ProtocolNumber, host2IPv4Addr.AddressWithPrefix.Address, host1IPv4Addr.AddressWithPrefix.Address, header.UDPMinimumSize)
				sum = checksum.Checksum(nil, sum)
				u.SetChecksum(^u.CalculateChecksum(sum))
				ip := header.IPv4(hdr.Prepend(header.IPv4MinimumSize))
				ip.Encode(&header.IPv4Fields{
					TotalLength: header.IPv4MinimumSize + header.UDPMinimumSize,
					TTL:         ipv4.DefaultTTL,
					Protocol:    uint8(udp.ProtocolNumber),
					SrcAddr:     host2IPv4Addr.AddressWithPrefix.Address,
					DstAddr:     host1IPv4Addr.AddressWithPrefix.Address,
				})
				ip.SetChecksum(^ip.CalculateChecksum())
				pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
					Payload: buffer.MakeWithData(hdr.View()),
				})
				defer pkt.DecRef()
				e.InjectInbound(ipv4.ProtocolNumber, pkt)
			},
			checkResp: func(t *testing.T, e *channel.Endpoint) {
				p := e.Read()
				if p == nil {
					t.Fatalf("timed out waiting for packet")
				}
				defer p.DecRef()
				if p.NetworkProtocolNumber != header.IPv4ProtocolNumber {
					t.Errorf("got p.NetworkProtocolNumber = %d, want = %d", p.NetworkProtocolNumber, header.IPv4ProtocolNumber)
				}
				if p.EgressRoute.RemoteLinkAddress != host2NICLinkAddr {
					t.Errorf("got p.EgressRoute.RemoteLinkAddress = %s, want = %s", p.EgressRoute.RemoteLinkAddress, host2NICLinkAddr)
				}
				payload := stack.PayloadSince(p.NetworkHeader())
				defer payload.Release()
				checker.IPv4(t, payload,
					checker.SrcAddr(host1IPv4Addr.AddressWithPrefix.Address),
					checker.DstAddr(host2IPv4Addr.AddressWithPrefix.Address),
					checker.ICMPv4(
						checker.ICMPv4Type(header.ICMPv4DstUnreachable),
						checker.ICMPv4Code(header.ICMPv4PortUnreachable)))
			},
		},

		{
			name: "Ping",
			rxPkt: func(e *channel.Endpoint) {
				totalLen := header.IPv4MinimumSize + header.ICMPv4MinimumSize
				hdr := prependable.New(totalLen)
				pkt := header.ICMPv4(hdr.Prepend(header.ICMPv4MinimumSize))
				pkt.SetType(header.ICMPv4Echo)
				pkt.SetCode(0)
				pkt.SetChecksum(0)
				pkt.SetChecksum(^checksum.Checksum(pkt, 0))
				ip := header.IPv4(hdr.Prepend(header.IPv4MinimumSize))
				ip.Encode(&header.IPv4Fields{
					TotalLength: uint16(totalLen),
					Protocol:    uint8(icmp.ProtocolNumber4),
					TTL:         ipv4.DefaultTTL,
					SrcAddr:     host2IPv4Addr.AddressWithPrefix.Address,
					DstAddr:     host1IPv4Addr.AddressWithPrefix.Address,
				})
				ip.SetChecksum(^ip.CalculateChecksum())
				echoPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
					Payload: buffer.MakeWithData(hdr.View()),
				})
				defer echoPkt.DecRef()
				e.InjectInbound(header.IPv4ProtocolNumber, echoPkt)
			},
			checkResp: func(t *testing.T, e *channel.Endpoint) {
				p := e.Read()
				if p == nil {
					t.Fatalf("timed out waiting for packet")
				}
				defer p.DecRef()
				if p.NetworkProtocolNumber != header.IPv4ProtocolNumber {
					t.Errorf("got p.NetworkProtocolNumber = %d, want = %d", p.NetworkProtocolNumber, header.IPv4ProtocolNumber)
				}
				if p.EgressRoute.RemoteLinkAddress != host2NICLinkAddr {
					t.Errorf("got p.EgressRoute.RemoteLinkAddress = %s, want = %s", p.EgressRoute.RemoteLinkAddress, host2NICLinkAddr)
				}
				payload := stack.PayloadSince(p.NetworkHeader())
				defer payload.Release()
				checker.IPv4(t, payload,
					checker.SrcAddr(host1IPv4Addr.AddressWithPrefix.Address),
					checker.DstAddr(host2IPv4Addr.AddressWithPrefix.Address),
					checker.ICMPv4(
						checker.ICMPv4Type(header.ICMPv4EchoReply),
						checker.ICMPv4Code(header.ICMPv4UnusedCode)))
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := newTestContext()
			defer ctx.cleanup()
			s := ctx.s
			clock := ctx.clock

			e := channel.New(1, defaultMTU, host1NICLinkAddr)
			defer e.Close()
			e.LinkEPCapabilities |= stack.CapabilityResolutionRequired
			if err := s.CreateNIC(nicID, e); err != nil {
				t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err)
			}
			if err := s.AddProtocolAddress(nicID, host1IPv4Addr, stack.AddressProperties{}); err != nil {
				t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, host1IPv4Addr, err)
			}

			s.SetRouteTable([]tcpip.Route{
				{
					Destination: host1IPv4Addr.AddressWithPrefix.Subnet(),
					NIC:         nicID,
				},
			})

			// Receive a packet to trigger link resolution before a response is sent.
			test.rxPkt(e)

			// Wait for a ARP request since link address resolution should be
			// performed.
			{
				clock.RunImmediatelyScheduledJobs()
				p := e.Read()
				if p == nil {
					t.Fatalf("timed out waiting for packet")
				}
				if p.NetworkProtocolNumber != arp.ProtocolNumber {
					t.Errorf("got p.NetworkProtocolNumber = %d, want = %d", p.NetworkProtocolNumber, arp.ProtocolNumber)
				}
				if p.EgressRoute.RemoteLinkAddress != header.EthernetBroadcastAddress {
					t.Errorf("got p.EgressRoute.RemoteLinkAddress = %s, want = %s", p.EgressRoute.RemoteLinkAddress, header.EthernetBroadcastAddress)
				}
				rep := header.ARP(p.NetworkHeader().Slice())
				p.DecRef()
				if got := rep.Op(); got != header.ARPRequest {
					t.Errorf("got Op() = %d, want = %d", got, header.ARPRequest)
				}
				if got := tcpip.LinkAddress(rep.HardwareAddressSender()); got != host1NICLinkAddr {
					t.Errorf("got HardwareAddressSender = %s, want = %s", got, host1NICLinkAddr)
				}
				if got := tcpip.AddrFromSlice(rep.ProtocolAddressSender()); got != host1IPv4Addr.AddressWithPrefix.Address {
					t.Errorf("got ProtocolAddressSender = %s, want = %s", got, host1IPv4Addr.AddressWithPrefix.Address)
				}
				if got := tcpip.AddrFromSlice(rep.ProtocolAddressTarget()); got != host2IPv4Addr.AddressWithPrefix.Address {
					t.Errorf("got ProtocolAddressTarget = %s, want = %s", got, host2IPv4Addr.AddressWithPrefix.Address)
				}
			}

			// Send an ARP reply to complete link address resolution.
			{
				hdr := make([]byte, header.ARPSize)
				packet := header.ARP(hdr)
				packet.SetIPv4OverEthernet()
				packet.SetOp(header.ARPReply)
				copy(packet.HardwareAddressSender(), host2NICLinkAddr)
				copy(packet.ProtocolAddressSender(), host2IPv4Addr.AddressWithPrefix.Address.AsSlice())
				copy(packet.HardwareAddressTarget(), host1NICLinkAddr)
				copy(packet.ProtocolAddressTarget(), host1IPv4Addr.AddressWithPrefix.Address.AsSlice())
				pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
					Payload: buffer.MakeWithData(hdr),
				})
				e.InjectInbound(arp.ProtocolNumber, pkt)
				pkt.DecRef()
			}

			// Expect the response now that the link address has resolved.
			clock.RunImmediatelyScheduledJobs()
			test.checkResp(t, e)

			// Since link resolution was already performed, it shouldn't be performed
			// again.
			test.rxPkt(e)
			test.checkResp(t, e)
		})
	}
}

// TestCloseLocking test that lock ordering is followed when closing an
// endpoint.
func TestCloseLocking(t *testing.T) {
	const (
		nicID1 = 1
		nicID2 = 2

		iterations = 1000
	)

	var (
		src = testutil.MustParse4("16.0.0.1")
		dst = testutil.MustParse4("16.0.0.2")
	)

	ctx := newTestContext()
	defer ctx.cleanup()
	s := ctx.s

	// Perform NAT so that the endpoint tries to search for a sibling endpoint
	// which ends up taking the protocol and endpoint lock (in that order).
	table := stack.Table{
		Rules: []stack.Rule{
			{Target: &stack.AcceptTarget{NetworkProtocol: header.IPv4ProtocolNumber}},
			{Target: &stack.AcceptTarget{NetworkProtocol: header.IPv4ProtocolNumber}},
			{Target: &stack.RedirectTarget{Port: 5, NetworkProtocol: header.IPv4ProtocolNumber}},
			{Target: &stack.AcceptTarget{NetworkProtocol: header.IPv4ProtocolNumber}},
			{Target: &stack.ErrorTarget{NetworkProtocol: header.IPv4ProtocolNumber}},
		},
		BuiltinChains: [stack.NumHooks]int{
			stack.Prerouting:  0,
			stack.Input:       1,
			stack.Forward:     stack.HookUnset,
			stack.Output:      2,
			stack.Postrouting: 3,
		},
		Underflows: [stack.NumHooks]int{
			stack.Prerouting:  0,
			stack.Input:       1,
			stack.Forward:     stack.HookUnset,
			stack.Output:      2,
			stack.Postrouting: 3,
		},
	}
	s.IPTables().ForceReplaceTable(stack.NATID, table, false /* ipv6 */)

	e := channel.New(0, defaultMTU, "")
	defer e.Close()
	if err := s.CreateNIC(nicID1, e); err != nil {
		t.Fatalf("CreateNIC(%d, _): %s", nicID1, err)
	}

	protocolAddr := tcpip.ProtocolAddress{
		Protocol:          ipv4.ProtocolNumber,
		AddressWithPrefix: src.WithPrefix(),
	}
	if err := s.AddProtocolAddress(nicID1, protocolAddr, stack.AddressProperties{}); err != nil {
		t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID1, protocolAddr, err)
	}

	s.SetRouteTable([]tcpip.Route{{
		Destination: header.IPv4EmptySubnet,
		NIC:         nicID1,
	}})

	var wq waiter.Queue
	ep, err := s.NewEndpoint(udp.ProtocolNumber, ipv4.ProtocolNumber, &wq)
	if err != nil {
		t.Fatal(err)
	}
	defer ep.Close()

	addr := tcpip.FullAddress{NIC: nicID1, Addr: dst, Port: 53}
	if err := ep.Connect(addr); err != nil {
		t.Errorf("ep.Connect(%#v): %s", addr, err)
	}

	var wg sync.WaitGroup
	defer wg.Wait()

	// Writing packets should trigger NAT which requires the stack to search the
	// protocol for network endpoints with the destination address.
	//
	// Creating and removing interfaces should modify the protocol and endpoint
	// which requires taking the locks of each.
	//
	// We expect the protocol > endpoint lock ordering to be followed here.
	wg.Add(2)
	go func() {
		defer wg.Done()

		data := []byte{1, 2, 3, 4}

		for i := 0; i < iterations; i++ {
			var r bytes.Reader
			r.Reset(data)
			if n, err := ep.Write(&r, tcpip.WriteOptions{}); err != nil {
				t.Errorf("ep.Write(_, _): %s", err)
				return
			} else if want := int64(len(data)); n != want {
				t.Errorf("got ep.Write(_, _) = (%d, _), want = (%d, _)", n, want)
				return
			}
		}
	}()
	go func() {
		defer wg.Done()

		for i := 0; i < iterations; i++ {
			ch := channel.New(0, defaultMTU, "")
			defer ch.Close()
			if err := s.CreateNIC(nicID2, ch); err != nil {
				t.Errorf("CreateNIC(%d, _): %s", nicID2, err)
				return
			}
			if err := s.RemoveNIC(nicID2); err != nil {
				t.Errorf("RemoveNIC(%d): %s", nicID2, err)
				return
			}
		}
	}()
}

func TestICMPEchoDefaultHandlerControlsReply(t *testing.T) {
	var (
		localAddr = tcpip.ProtocolAddress{
			Protocol: ipv4.ProtocolNumber,
			AddressWithPrefix: tcpip.AddressWithPrefix{
				Address:   tcpip.AddrFromSlice(net.ParseIP("192.168.0.1").To4()),
				PrefixLen: 24,
			},
		}
		remoteAddr = tcpip.ProtocolAddress{
			Protocol: ipv4.ProtocolNumber,
			AddressWithPrefix: tcpip.AddressWithPrefix{
				Address:   tcpip.AddrFromSlice(net.ParseIP("192.168.0.2").To4()),
				PrefixLen: 24,
			},
		}
	)

	tests := []struct {
		name              string
		installHandler    bool
		handled           bool
		wantHandlerCalled bool
		wantReply         bool
	}{
		{
			name:      "no default handler",
			wantReply: true,
		},
		{
			name:              "default handler handled",
			installHandler:    true,
			handled:           true,
			wantHandlerCalled: true,
			wantReply:         false,
		},
		{
			name:              "default handler not handled",
			installHandler:    true,
			handled:           false,
			wantHandlerCalled: true,
			wantReply:         true,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			clock := faketime.NewManualClock()
			s := stack.New(stack.Options{
				NetworkProtocols:   []stack.NetworkProtocolFactory{arp.NewProtocol, ipv4.NewProtocol},
				TransportProtocols: []stack.TransportProtocolFactory{icmp.NewProtocol4},
				Clock:              clock,
			})
			defer func() {
				s.Close()
				s.Wait()
				refs.DoRepeatedLeakCheck()
			}()

			const ident = 1234
			handlerCalled := false
			if test.installHandler {
				s.SetTransportProtocolHandler(icmp.ProtocolNumber4, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
					handlerCalled = true
					if got := id.LocalPort; got != ident {
						t.Errorf("got id.LocalPort = %d, want = %d", got, ident)
					}
					return test.handled
				})
			}

			e := channel.New(1, defaultMTU, "")
			defer e.Close()
			if err := s.CreateNIC(nicID, e); err != nil {
				t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err)
			}
			if err := s.AddProtocolAddress(nicID, localAddr, stack.AddressProperties{}); err != nil {
				t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, localAddr, err)
			}
			s.SetRouteTable([]tcpip.Route{{
				Destination: localAddr.AddressWithPrefix.Subnet(),
				NIC:         nicID,
			}})

			totalLength := header.IPv4MinimumSize + header.ICMPv4MinimumSize
			hdr := prependable.New(totalLength)
			icmpH := header.ICMPv4(hdr.Prepend(header.ICMPv4MinimumSize))
			icmpH.SetIdent(ident)
			icmpH.SetType(header.ICMPv4Echo)
			icmpH.SetCode(header.ICMPv4UnusedCode)
			icmpH.SetChecksum(0)
			icmpH.SetChecksum(^checksum.Checksum(icmpH, 0))
			ip := header.IPv4(hdr.Prepend(header.IPv4MinimumSize))
			ip.Encode(&header.IPv4Fields{
				TotalLength: uint16(totalLength),
				Protocol:    uint8(icmp.ProtocolNumber4),
				TTL:         ipv4.DefaultTTL,
				SrcAddr:     remoteAddr.AddressWithPrefix.Address,
				DstAddr:     localAddr.AddressWithPrefix.Address,
			})
			ip.SetChecksum(^ip.CalculateChecksum())
			echoPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
				Payload: buffer.MakeWithData(hdr.View()),
			})
			e.InjectInbound(header.IPv4ProtocolNumber, echoPkt)
			echoPkt.DecRef()
			clock.RunImmediatelyScheduledJobs()

			if got, want := handlerCalled, test.wantHandlerCalled; got != want {
				t.Fatalf("got handlerCalled = %t, want = %t", got, want)
			}

			p := e.Read()
			if !test.wantReply {
				if p != nil {
					p.DecRef()
					t.Fatalf("got unexpected ICMP echo reply")
				}
				return
			}
			if p == nil {
				t.Fatalf("expected ICMP echo reply")
			}
			defer p.DecRef()
			payload := stack.PayloadSince(p.NetworkHeader())
			defer payload.Release()
			checker.IPv4(t, payload,
				checker.SrcAddr(localAddr.AddressWithPrefix.Address),
				checker.DstAddr(remoteAddr.AddressWithPrefix.Address),
				checker.ICMPv4(
					checker.ICMPv4Type(header.ICMPv4EchoReply),
					checker.ICMPv4Code(header.ICMPv4UnusedCode)))
		})
	}
}

func TestICMPEchoRegisteredEndpointDoesNotSuppressReply(t *testing.T) {
	localAddr := tcpip.ProtocolAddress{
		Protocol: ipv4.ProtocolNumber,
		AddressWithPrefix: tcpip.AddressWithPrefix{
			Address:   tcpip.AddrFromSlice(net.ParseIP("192.168.0.1").To4()),
			PrefixLen: 24,
		},
	}
	remoteAddr := tcpip.ProtocolAddress{
		Protocol: ipv4.ProtocolNumber,
		AddressWithPrefix: tcpip.AddressWithPrefix{
			Address:   tcpip.AddrFromSlice(net.ParseIP("192.168.0.2").To4()),
			PrefixLen: 24,
		},
	}

	clock := faketime.NewManualClock()
	s := stack.New(stack.Options{
		NetworkProtocols:   []stack.NetworkProtocolFactory{arp.NewProtocol, ipv4.NewProtocol},
		TransportProtocols: []stack.TransportProtocolFactory{icmp.NewProtocol4},
		Clock:              clock,
	})
	defer func() {
		s.Close()
		s.Wait()
		refs.DoRepeatedLeakCheck()
	}()

	e := channel.New(1, defaultMTU, "")
	defer e.Close()
	if err := s.CreateNIC(nicID, e); err != nil {
		t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err)
	}
	if err := s.AddProtocolAddress(nicID, localAddr, stack.AddressProperties{}); err != nil {
		t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, localAddr, err)
	}
	s.SetRouteTable([]tcpip.Route{{
		Destination: localAddr.AddressWithPrefix.Subnet(),
		NIC:         nicID,
	}})

	const ident = 1234
	var wq waiter.Queue
	ep, err := s.NewEndpoint(icmp.ProtocolNumber4, ipv4.ProtocolNumber, &wq)
	if err != nil {
		t.Fatalf("s.NewEndpoint(%d, %d, _) = %s", icmp.ProtocolNumber4, ipv4.ProtocolNumber, err)
	}
	defer ep.Close()
	if err := ep.Bind(tcpip.FullAddress{Addr: localAddr.AddressWithPrefix.Address, Port: ident}); err != nil {
		t.Fatalf("ep.Bind(...) = %s", err)
	}

	handlerCalled := false
	s.SetTransportProtocolHandler(icmp.ProtocolNumber4, func(stack.TransportEndpointID, *stack.PacketBuffer) bool {
		handlerCalled = true
		return true
	})

	totalLength := header.IPv4MinimumSize + header.ICMPv4MinimumSize
	hdr := prependable.New(totalLength)
	icmpH := header.ICMPv4(hdr.Prepend(header.ICMPv4MinimumSize))
	icmpH.SetIdent(ident)
	icmpH.SetType(header.ICMPv4Echo)
	icmpH.SetCode(header.ICMPv4UnusedCode)
	icmpH.SetChecksum(0)
	icmpH.SetChecksum(^checksum.Checksum(icmpH, 0))
	ip := header.IPv4(hdr.Prepend(header.IPv4MinimumSize))
	ip.Encode(&header.IPv4Fields{
		TotalLength: uint16(totalLength),
		Protocol:    uint8(icmp.ProtocolNumber4),
		TTL:         ipv4.DefaultTTL,
		SrcAddr:     remoteAddr.AddressWithPrefix.Address,
		DstAddr:     localAddr.AddressWithPrefix.Address,
	})
	ip.SetChecksum(^ip.CalculateChecksum())
	echoPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
		Payload: buffer.MakeWithData(hdr.View()),
	})
	e.InjectInbound(header.IPv4ProtocolNumber, echoPkt)
	echoPkt.DecRef()
	clock.RunImmediatelyScheduledJobs()

	if handlerCalled {
		t.Fatalf("default handler was unexpectedly called")
	}

	p := e.Read()
	if p == nil {
		t.Fatalf("expected ICMP echo reply")
	}
	defer p.DecRef()
	payload := stack.PayloadSince(p.NetworkHeader())
	defer payload.Release()
	checker.IPv4(t, payload,
		checker.SrcAddr(localAddr.AddressWithPrefix.Address),
		checker.DstAddr(remoteAddr.AddressWithPrefix.Address),
		checker.ICMPv4(
			checker.ICMPv4Type(header.ICMPv4EchoReply),
			checker.ICMPv4Code(header.ICMPv4UnusedCode)))
}

func TestICMPEchoTemporaryAddressSuppressesReply(t *testing.T) {
	assignedAddr := tcpip.ProtocolAddress{
		Protocol: ipv4.ProtocolNumber,
		AddressWithPrefix: tcpip.AddressWithPrefix{
			Address:   tcpip.AddrFromSlice(net.ParseIP("192.168.0.1").To4()),
			PrefixLen: 24,
		},
	}
	temporaryAddr := tcpip.AddrFromSlice(net.ParseIP("192.168.0.99").To4())
	remoteAddr := tcpip.ProtocolAddress{
		Protocol: ipv4.ProtocolNumber,
		AddressWithPrefix: tcpip.AddressWithPrefix{
			Address:   tcpip.AddrFromSlice(net.ParseIP("192.168.0.2").To4()),
			PrefixLen: 24,
		},
	}

	clock := faketime.NewManualClock()
	s := stack.New(stack.Options{
		NetworkProtocols:   []stack.NetworkProtocolFactory{arp.NewProtocol, ipv4.NewProtocol},
		TransportProtocols: []stack.TransportProtocolFactory{icmp.NewProtocol4},
		Clock:              clock,
	})
	defer func() {
		s.Close()
		s.Wait()
		refs.DoRepeatedLeakCheck()
	}()

	e := channel.New(1, defaultMTU, "")
	defer e.Close()
	if err := s.CreateNIC(nicID, e); err != nil {
		t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err)
	}
	if err := s.AddProtocolAddress(nicID, assignedAddr, stack.AddressProperties{}); err != nil {
		t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, assignedAddr, err)
	}
	if err := s.SetPromiscuousMode(nicID, true); err != nil {
		t.Fatalf("s.SetPromiscuousMode(%d, true): %s", nicID, err)
	}
	s.SetRouteTable([]tcpip.Route{{
		Destination: assignedAddr.AddressWithPrefix.Subnet(),
		NIC:         nicID,
	}})

	const ident = 1234
	totalLength := header.IPv4MinimumSize + header.ICMPv4MinimumSize
	hdr := prependable.New(totalLength)
	icmpH := header.ICMPv4(hdr.Prepend(header.ICMPv4MinimumSize))
	icmpH.SetIdent(ident)
	icmpH.SetType(header.ICMPv4Echo)
	icmpH.SetCode(header.ICMPv4UnusedCode)
	icmpH.SetChecksum(0)
	icmpH.SetChecksum(^checksum.Checksum(icmpH, 0))
	ip := header.IPv4(hdr.Prepend(header.IPv4MinimumSize))
	ip.Encode(&header.IPv4Fields{
		TotalLength: uint16(totalLength),
		Protocol:    uint8(icmp.ProtocolNumber4),
		TTL:         ipv4.DefaultTTL,
		SrcAddr:     remoteAddr.AddressWithPrefix.Address,
		DstAddr:     temporaryAddr,
	})
	ip.SetChecksum(^ip.CalculateChecksum())
	echoPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
		Payload: buffer.MakeWithData(hdr.View()),
	})
	e.InjectInbound(header.IPv4ProtocolNumber, echoPkt)
	echoPkt.DecRef()
	clock.RunImmediatelyScheduledJobs()

	if p := e.Read(); p != nil {
		p.DecRef()
		t.Fatalf("got unexpected ICMP echo reply")
	}
}

func TestIcmpRateLimit(t *testing.T) {
	var (
		host1IPv4Addr = tcpip.ProtocolAddress{
			Protocol: ipv4.ProtocolNumber,
			AddressWithPrefix: tcpip.AddressWithPrefix{
				Address:   tcpip.AddrFromSlice(net.ParseIP("192.168.0.1").To4()),
				PrefixLen: 24,
			},
		}
		host2IPv4Addr = tcpip.ProtocolAddress{
			Protocol: ipv4.ProtocolNumber,
			AddressWithPrefix: tcpip.AddressWithPrefix{
				Address:   tcpip.AddrFromSlice(net.ParseIP("192.168.0.2").To4()),
				PrefixLen: 24,
			},
		}
	)
	ctx := newTestContext()
	defer ctx.cleanup()
	s := ctx.s

	const icmpBurst = 5
	s.SetICMPBurst(icmpBurst)

	e := channel.New(1, defaultMTU, tcpip.LinkAddress(""))
	defer e.Close()
	if err := s.CreateNIC(nicID, e); err != nil {
		t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err)
	}
	if err := s.AddProtocolAddress(nicID, host1IPv4Addr, stack.AddressProperties{}); err != nil {
		t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, host1IPv4Addr, err)
	}
	s.SetRouteTable([]tcpip.Route{
		{
			Destination: host1IPv4Addr.AddressWithPrefix.Subnet(),
			NIC:         nicID,
		},
	})
	tests := []struct {
		name         string
		createPacket func() []byte
		check        func(*testing.T, *channel.Endpoint, int)
	}{
		{
			name: "echo",
			createPacket: func() []byte {
				totalLength := header.IPv4MinimumSize + header.ICMPv4MinimumSize
				hdr := prependable.New(totalLength)
				icmpH := header.ICMPv4(hdr.Prepend(header.ICMPv4MinimumSize))
				icmpH.SetIdent(1)
				icmpH.SetSequence(1)
				icmpH.SetType(header.ICMPv4Echo)
				icmpH.SetCode(header.ICMPv4UnusedCode)
				icmpH.SetChecksum(0)
				icmpH.SetChecksum(^checksum.Checksum(icmpH, 0))
				ip := header.IPv4(hdr.Prepend(header.IPv4MinimumSize))
				ip.Encode(&header.IPv4Fields{
					TotalLength: uint16(totalLength),
					Protocol:    uint8(header.ICMPv4ProtocolNumber),
					TTL:         1,
					SrcAddr:     host2IPv4Addr.AddressWithPrefix.Address,
					DstAddr:     host1IPv4Addr.AddressWithPrefix.Address,
				})
				ip.SetChecksum(^ip.CalculateChecksum())
				return hdr.View()
			},
			check: func(t *testing.T, e *channel.Endpoint, round int) {
				p := e.Read()
				if p == nil {
					t.Fatalf("expected echo response, no packet read in endpoint in round %d", round)
				}
				defer p.DecRef()
				if got, want := p.NetworkProtocolNumber, header.IPv4ProtocolNumber; got != want {
					t.Errorf("got p.NetworkProtocolNumber = %d, want = %d", got, want)
				}
				payload := stack.PayloadSince(p.NetworkHeader())
				defer payload.Release()
				checker.IPv4(t, payload,
					checker.SrcAddr(host1IPv4Addr.AddressWithPrefix.Address),
					checker.DstAddr(host2IPv4Addr.AddressWithPrefix.Address),
					checker.ICMPv4(
						checker.ICMPv4Type(header.ICMPv4EchoReply),
					))
			},
		},
		{
			name: "dst unreachable",
			createPacket: func() []byte {
				totalLength := header.IPv4MinimumSize + header.UDPMinimumSize
				hdr := prependable.New(totalLength)
				udpH := header.UDP(hdr.Prepend(header.UDPMinimumSize))
				udpH.Encode(&header.UDPFields{
					SrcPort: 100,
					DstPort: 101,
					Length:  header.UDPMinimumSize,
				})
				ip := header.IPv4(hdr.Prepend(header.IPv4MinimumSize))
				ip.Encode(&header.IPv4Fields{
					TotalLength: uint16(totalLength),
					Protocol:    uint8(header.UDPProtocolNumber),
					TTL:         1,
					SrcAddr:     host2IPv4Addr.AddressWithPrefix.Address,
					DstAddr:     host1IPv4Addr.AddressWithPrefix.Address,
				})
				ip.SetChecksum(^ip.CalculateChecksum())
				return hdr.View()
			},
			check: func(t *testing.T, e *channel.Endpoint, round int) {
				p := e.Read()
				if round >= icmpBurst {
					if p != nil {
						t.Errorf("got packet %x in round %d, expected ICMP rate limit to stop it", p.Data().AsRange().ToSlice(), round)
						p.DecRef()
					}
					return
				}
				if p == nil {
					t.Fatalf("expected unreachable in round %d, no packet read in endpoint", round)
				}
				defer p.DecRef()
				payload := stack.PayloadSince(p.NetworkHeader())
				defer payload.Release()
				checker.IPv4(t, payload,
					checker.SrcAddr(host1IPv4Addr.AddressWithPrefix.Address),
					checker.DstAddr(host2IPv4Addr.AddressWithPrefix.Address),
					checker.ICMPv4(
						checker.ICMPv4Type(header.ICMPv4DstUnreachable),
					))
			},
		},
	}
	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			for round := 0; round < icmpBurst+1; round++ {
				pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
					Payload: buffer.MakeWithData(testCase.createPacket()),
				})
				e.InjectInbound(header.IPv4ProtocolNumber, pkt)
				pkt.DecRef()
				testCase.check(t, e, round)
			}
		})
	}
}

func newTCPPacket(t *testing.T, srcAddr, dstAddr tcpip.Address, ttl uint8, tcpChecksum uint16) *stack.PacketBuffer {
	t.Helper()
	ipHeaderLength := header.IPv4MinimumSize
	tcpHeaderLength := header.TCPMinimumSize
	totalLength := ipHeaderLength + tcpHeaderLength + 10 // 10 bytes payload
	hdr := prependable.New(totalLength)

	// Payload
	hdr.Prepend(10)
	copy(hdr.View(), []byte("1234567890"))

	// TCP Header
	tcpH := header.TCP(hdr.Prepend(tcpHeaderLength))
	tcpH.Encode(&header.TCPFields{
		SrcPort:    1234,
		DstPort:    80,
		SeqNum:     100,
		AckNum:     200,
		DataOffset: uint8(tcpHeaderLength),
		Flags:      header.TCPFlagSyn,
		WindowSize: 65535,
	})
	tcpH.SetChecksum(tcpChecksum)

	// IP Header
	ipH := header.IPv4(hdr.Prepend(ipHeaderLength))
	ipH.Encode(&header.IPv4Fields{
		TotalLength: uint16(totalLength),
		Protocol:    uint8(header.TCPProtocolNumber),
		TTL:         ttl,
		SrcAddr:     srcAddr,
		DstAddr:     dstAddr,
	})
	ipH.SetChecksum(0)
	ipH.SetChecksum(^ipH.CalculateChecksum())

	pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
		Payload: buffer.MakeWithData(hdr.View()),
	})
	pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber
	return pkt
}

func TestForwardingTCPChecksum(t *testing.T) {
	ctx := newTestContext()
	defer ctx.cleanup()
	s := ctx.s

	endpoints := make(map[tcpip.NICID]*channel.Endpoint)
	for nicID, addr := range defaultEndpointConfigs {
		ep := channel.New(1, ipv4.MaxTotalSize, "")
		defer ep.Close()

		if err := s.CreateNIC(nicID, ep); err != nil {
			t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err)
		}
		addr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: addr}
		if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil {
			t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, addr, err)
		}
		endpoints[nicID] = ep
	}

	s.SetRouteTable([]tcpip.Route{
		{
			Destination: incomingIPv4Addr.Subnet(),
			NIC:         incomingNICID,
		},
		{
			Destination: outgoingIPv4Addr.Subnet(),
			NIC:         outgoingNICID,
		},
	})

	if err := s.SetForwardingDefaultAndAllNICs(header.IPv4ProtocolNumber, true); err != nil {
		t.Fatalf("s.SetForwardingDefaultAndAllNICs(%d, true): %s", header.IPv4ProtocolNumber, err)
	}

	// Inject a TCP packet with checksum 0 (invalid) into incoming NIC.
	requestPkt := newTCPPacket(t, remoteIPv4Addr1, remoteIPv4Addr2, 64, 0)
	defer requestPkt.DecRef()

	incomingEndpoint := endpoints[incomingNICID]
	incomingEndpoint.InjectInbound(header.IPv4ProtocolNumber, requestPkt)

	outgoingEndpoint := endpoints[outgoingNICID]
	reply := outgoingEndpoint.Read()
	if reply == nil {
		t.Fatal("Expected forwarded TCP packet through outgoing NIC")
	}
	defer reply.DecRef()

	// Verify that the forwarded packet has a valid TCP checksum.
	payload := stack.PayloadSince(reply.NetworkHeader())
	defer payload.Release()

	ipv4Header := header.IPv4(payload.AsSlice())
	tcpHeaderBytes := ipv4Header.Payload()
	tcpHeader := header.TCP(tcpHeaderBytes)

	src := ipv4Header.SourceAddress()
	dst := ipv4Header.DestinationAddress()
	payloadLength := uint16(len(tcpHeader.Payload()))
	payloadCsum := checksum.Checksum(tcpHeader.Payload(), 0)
	if !tcpHeader.IsChecksumValid(src, dst, payloadCsum, payloadLength) {
		t.Errorf("expected valid TCP checksum, but got invalid")
	}
}

func TestForwardedICMPInnerIPv4OptionsPanics(t *testing.T) {
	ctx := newTestContext()
	defer ctx.cleanup()
	s := ctx.s

	endpoints := make(map[tcpip.NICID]*channel.Endpoint)
	for nicID, addr := range defaultEndpointConfigs {
		ep := channel.New(1, ipv4.MaxTotalSize, "")
		defer ep.Close()
		if err := s.CreateNIC(nicID, ep); err != nil {
			t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err)
		}
		protoAddr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: addr}
		if err := s.AddProtocolAddress(nicID, protoAddr, stack.AddressProperties{}); err != nil {
			t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, protoAddr, err)
		}
		endpoints[nicID] = ep
	}

	s.SetRouteTable([]tcpip.Route{
		{Destination: incomingIPv4Addr.Subnet(), NIC: incomingNICID},
		{Destination: outgoingIPv4Addr.Subnet(), NIC: outgoingNICID},
	})

	if err := s.SetForwardingDefaultAndAllNICs(header.IPv4ProtocolNumber, true); err != nil {
		t.Fatalf("s.SetForwardingDefaultAndAllNICs(%d, true): %s", header.IPv4ProtocolNumber, err)
	}

	// Inner IPv4 header with IHL=6 (24 bytes -> has options).
	inner := header.IPv4(make([]byte, 24))
	inner.Encode(&header.IPv4Fields{
		TotalLength: 24,
		TTL:         64,
		Protocol:    uint8(header.UDPProtocolNumber),
		SrcAddr:     remoteIPv4Addr1,
		DstAddr:     remoteIPv4Addr2,
	})
	inner.SetHeaderLength(24)

	icmp := header.ICMPv4(make([]byte, header.ICMPv4MinimumSize+len(inner)))
	icmp.SetType(header.ICMPv4DstUnreachable)
	icmp.SetCode(header.ICMPv4PortUnreachable)
	copy(icmp.Payload(), inner)

	// Outer: remote-to-remote (10.0.0.2 -> 11.0.0.2) so the packet is forwarded.
	outer := header.IPv4(make([]byte, header.IPv4MinimumSize+len(icmp)))
	outer.Encode(&header.IPv4Fields{
		TotalLength: uint16(len(outer)),
		TTL:         64,
		Protocol:    uint8(header.ICMPv4ProtocolNumber),
		SrcAddr:     remoteIPv4Addr1,
		DstAddr:     remoteIPv4Addr2,
	})
	copy(outer[header.IPv4MinimumSize:], icmp)
	outer.SetChecksum(0)
	outer.SetChecksum(^outer.CalculateChecksum())

	pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
		Payload: buffer.MakeWithData(outer),
	})
	defer pkt.DecRef()

	endpoints[incomingNICID].InjectInbound(header.IPv4ProtocolNumber, pkt)
}
