// Copyright 2020 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ipv6_fragment_reassembly_test

import (
	"flag"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"gvisor.dev/gvisor/pkg/rand"
	"gvisor.dev/gvisor/pkg/tcpip"
	"gvisor.dev/gvisor/pkg/tcpip/checksum"
	"gvisor.dev/gvisor/pkg/tcpip/header"
	"gvisor.dev/gvisor/test/packetimpact/testbench"
)

func init() {
	testbench.Initialize(flag.CommandLine)
}

type fragmentInfo struct {
	offset uint16
	size   uint16
	more   bool
	id     uint32
}

func TestIPv6FragmentReassembly(t *testing.T) {
	icmpv6ProtoNum := header.IPv6ExtensionHeaderIdentifier(header.ICMPv6ProtocolNumber)

	tests := []struct {
		description  string
		ipPayloadLen int
		fragments    []fragmentInfo
		expectReply  bool
	}{
		{
			description:  "basic reassembly",
			ipPayloadLen: 3000,
			fragments: []fragmentInfo{
				{offset: 0, size: 1000, id: 100, more: true},
				{offset: 1000, size: 1000, id: 100, more: true},
				{offset: 2000, size: 1000, id: 100, more: false},
			},
			expectReply: true,
		},
		{
			description:  "out of order fragments",
			ipPayloadLen: 3000,
			fragments: []fragmentInfo{
				{offset: 0, size: 1000, id: 101, more: true},
				{offset: 2000, size: 1000, id: 101, more: false},
				{offset: 1000, size: 1000, id: 101, more: true},
			},
			expectReply: true,
		},
		{
			description:  "duplicated fragments",
			ipPayloadLen: 3000,
			fragments: []fragmentInfo{
				{offset: 0, size: 1000, id: 102, more: true},
				{offset: 1000, size: 1000, id: 102, more: true},
				{offset: 1000, size: 1000, id: 102, more: true},
				{offset: 2000, size: 1000, id: 102, more: false},
			},
			expectReply: true,
		},
		{
			description:  "fragment subset",
			ipPayloadLen: 3000,
			fragments: []fragmentInfo{
				{offset: 512, size: 256, id: 103, more: true},
				// Overlapping fragment must be dropped.
				// rfc8200#section-4.5
				// However, the linux kernel may accept overlaps
				// depending on the order of the fragments.
				// For example, if we switch the first and the second fragment,
				// the linux kernel will accept the packet and the test will fail.
				{offset: 0, size: 1000, id: 103, more: true},
				{offset: 1000, size: 1000, id: 103, more: true},
				{offset: 2000, size: 1000, id: 103, more: false},
			},
			expectReply: false,
		},
		{
			description:  "fragment overlap",
			ipPayloadLen: 3000,
			fragments: []fragmentInfo{
				{offset: 0, size: 1000, id: 104, more: true},
				{offset: 1512, size: 1000, id: 104, more: true},
				{offset: 1000, size: 1000, id: 104, more: true},
				{offset: 2000, size: 1000, id: 104, more: false},
			},
			expectReply: false,
		},
	}

	for _, test := range tests {
		t.Run(test.description, func(t *testing.T) {
			dut := testbench.NewDUT(t)
			conn := dut.Net.NewIPv6Conn(t, testbench.IPv6{}, testbench.IPv6{})
			defer conn.Close(t)

			lIP := tcpip.AddrFrom16Slice(dut.Net.LocalIPv6)
			rIP := tcpip.AddrFrom16Slice(dut.Net.RemoteIPv6)

			data := make([]byte, test.ipPayloadLen)
			icmp := header.ICMPv6(data[:header.ICMPv6HeaderSize])
			icmp.SetType(header.ICMPv6EchoRequest)
			icmp.SetCode(header.ICMPv6UnusedCode)
			icmp.SetChecksum(0)
			originalPayload := data[header.ICMPv6HeaderSize:]
			if _, err := rand.Read(originalPayload); err != nil {
				t.Fatalf("rand.Read: %s", err)
			}

			cksum := header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
				Header:      icmp,
				Src:         lIP,
				Dst:         rIP,
				PayloadCsum: checksum.Checksum(originalPayload, 0 /* initial */),
				PayloadLen:  len(originalPayload),
			})
			icmp.SetChecksum(cksum)

			for _, fragment := range test.fragments {
				conn.Send(t, testbench.IPv6{},
					&testbench.IPv6FragmentExtHdr{
						NextHeader:     &icmpv6ProtoNum,
						FragmentOffset: testbench.Uint16(fragment.offset / header.IPv6FragmentExtHdrFragmentOffsetBytesPerUnit),
						MoreFragments:  testbench.Bool(fragment.more),
						Identification: testbench.Uint32(fragment.id),
					},
					&testbench.Payload{
						Bytes: data[fragment.offset:][:fragment.size],
					})
			}

			var bytesReceived int
			reassembledPayload := make([]byte, test.ipPayloadLen)
			for {
				incomingFrame, err := conn.ExpectFrame(t, testbench.Layers{
					&testbench.Ether{},
					&testbench.IPv6{},
					&testbench.IPv6FragmentExtHdr{},
				}, time.Second)
				if err != nil {
					// Either an unexpected frame was received, or none at all.
					if test.expectReply && bytesReceived < test.ipPayloadLen {
						t.Fatalf("received %d bytes out of %d, then conn.ExpectFrame(_, _, time.Second) failed with %s", bytesReceived, test.ipPayloadLen, err)
					}
					break
				}
				if !test.expectReply {
					t.Fatalf("unexpected reply received:\n%s", incomingFrame)
				}
				ipPayload, err := incomingFrame[3 /* Payload */].ToBytes()
				if err != nil {
					t.Fatalf("failed to parse ICMPv6 header: incomingPacket[3].ToBytes() = (_, %s)", err)
				}
				offset := *incomingFrame[2 /* IPv6FragmentExtHdr */].(*testbench.IPv6FragmentExtHdr).FragmentOffset
				offset *= header.IPv6FragmentExtHdrFragmentOffsetBytesPerUnit
				if copied := copy(reassembledPayload[offset:], ipPayload); copied != len(ipPayload) {
					t.Fatalf("wrong number of bytes copied into reassembledPayload: got = %d, want = %d", copied, len(ipPayload))
				}
				bytesReceived += len(ipPayload)
			}

			if test.expectReply {
				if diff := cmp.Diff(originalPayload, reassembledPayload[header.ICMPv6HeaderSize:]); diff != "" {
					t.Fatalf("reassembledPayload mismatch (-want +got):\n%s", diff)
				}
			}
		})
	}
}
