// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//     http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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.

// IPv4 network packet verifier
package main

import (
	"errors"
	"flag"
	"fmt"
	"net"
	"os"
	"time"

	"github.com/google/gopacket"
	"github.com/google/gopacket/layers"
	"github.com/google/gopacket/pcap"
	"github.com/vishvananda/netlink"
)

const (
	shortDescription = "packet-verifier"
	longDescription  = "Packet verifier fetches corresponding interfaces and validates the packets."
)

var (
	// version string populated during build.
	version = "unknown"

	// ip to monitor on the interfaces
	ipAddress  string
	receiverIP string
	device     string

	// vlan ID to monitor on the interfaces
	vlanIDToMonitor int

	// pcap parameters (requires libpcap-devel to be installed on the host)
	snapshotLen int32 = 1024
	promiscuous       = false
	timeout           = 30 * time.Second
)

// eniConfig details regarding ENIs
type eniConfig struct {
	name                string
	shouldCheckSrc      bool
	shouldVerifyVlanTag bool
}

func main() {
	// list of enis to monitor
	var enis []eniConfig

	fmt.Print("Verifying packet flow...\n")

	helpFlag := flag.Bool("help", false, "displays usage information")
	versionFlag := flag.Bool("version", false, "displays version information")
	flag.StringVar(&ipAddress, "ip-to-monitor", "", "pod ip to monitor.")
	flag.StringVar(&receiverIP, "receiver-ip", "", "other IP that interacts with the pod.")
	flag.IntVar(&vlanIDToMonitor, "vlanid-to-monitor", 0, "pod vlan id to monitor.")
	flag.StringVar(&device, "host-device", "eth0", "host device of the node.")

	flag.Usage = printUsage

	// Parse command line flags.
	flag.Parse()

	if *helpFlag {
		printUsage()
		os.Exit(0)
	}

	if *versionFlag {
		printVersion()
		os.Exit(0)
	}

	if ipAddress == "" {
		fmt.Println("ip-to-monitor can't be empty")
		os.Exit(1)
	}
	ipToMonitor := net.ParseIP(ipAddress)

	hostName, err := os.Hostname()
	if err != nil {
		fmt.Printf("unable to retrieve the host name due to %+v", err)
		os.Exit(1)
	}

	// if its host ip then just use eth0 and skip rest of the operation
	if hostName == ipToMonitor.String() {
		hostENI := eniConfig{name: device}
		enis = append(enis, hostENI)
	} else {

		// read route tables to find the hostveth
		routeFilter := &netlink.Route{
			Table: vlanIDToMonitor + 100,
		}
		routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, routeFilter, netlink.RT_FILTER_TABLE)
		if err != nil {
			fmt.Printf("unable to get routes for table using vlanID %d. Error: %+v", vlanIDToMonitor, err)
			os.Exit(1)
		}
		for _, route := range routes {
			if route.Dst != nil && ipToMonitor.Equal(route.Dst.IP) {
				linkIndex := route.LinkIndex
				link, err := netlink.LinkByIndex(linkIndex)
				if err != nil {
					fmt.Printf("unable to find index %d error %+v", linkIndex, err)
					os.Exit(1)
				}
				hostVethToMonitor := eniConfig{name: link.Attrs().Name, shouldCheckSrc: true}
				enis = append(enis, hostVethToMonitor)
				break
			}
		}

		// get vlan devices
		if vlanIDToMonitor != 0 {
			link, err := netlink.LinkByName(fmt.Sprintf("vlan.eth.%d", vlanIDToMonitor))
			if err != nil {
				fmt.Printf("unable to get vlan device, error: %+v", err)
				os.Exit(1)
			}
			vlanDevToMonitor := eniConfig{name: link.Attrs().Name, shouldCheckSrc: true}
			enis = append(enis, vlanDevToMonitor)

			// find the trunk dev
			parentLink, err := netlink.LinkByIndex(link.Attrs().ParentIndex)
			if err != nil {
				fmt.Printf("unable to get parent link %d. Error %+v", link.Attrs().ParentIndex, err)
				os.Exit(1)
			}
			trunkDevToMonitor := eniConfig{name: parentLink.Attrs().Name, shouldVerifyVlanTag: true}
			enis = append(enis, trunkDevToMonitor)
		} else {
			// find the eni to monitor associated with the pod
			rules, err := netlink.RuleList(netlink.FAMILY_V4)
			if err != nil {
				fmt.Printf("unable to get ip rules due to %+v", err)
				os.Exit(1)
			}
			for _, rule := range rules {
				// Find the ENI in the route table associated with the pod
				if rule.Src != nil && ipToMonitor.Equal(rule.Src.IP) {
					routeFilter := &netlink.Route{
						Table: rule.Table,
					}
					routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, routeFilter, netlink.RT_FILTER_TABLE)
					if err != nil {
						fmt.Printf("unable to get routes based on iprules %+v", err)
						os.Exit(1)
					}

					if len(routes) > 0 {
						parentLink, err := netlink.LinkByIndex(routes[0].LinkIndex)
						if err != nil {
							fmt.Printf("unable to get parent ENI for the link %+v", err)
							os.Exit(1)
						}
						eniToMonitor := eniConfig{name: parentLink.Attrs().Name}
						enis = append(enis, eniToMonitor)
					}
				}
			}
			// if explicit route is not round then pod has to be associated with $device
			if len(enis) == 0 {
				eniToMonitor := eniConfig{name: device}
				enis = append(enis, eniToMonitor)
			}
		}
	}

	fmt.Printf("ENIs to monitor: %+v\n", enis)

	err = monitorPacketOnInterfaces(ipToMonitor, vlanIDToMonitor, enis)
	if err != nil {
		fmt.Printf("unable to verify packets on the interface %+v ", err)
		os.Exit(1)
	}

	fmt.Println("Successfully verified all the interfaces.")
}

// monitorPacketOnInterfaces invokes monitorPackets for each interface
func monitorPacketOnInterfaces(ipToMonitor net.IP, vlanIDToMonitor int, enis []eniConfig) error {

	for _, iface := range enis {
		fmt.Printf("Verifying interface: %+v\n", iface)
		err := monitorPackets(ipToMonitor, vlanIDToMonitor, iface)
		if err != nil {
			return err
		}
	}

	return nil
}

// monitorPackets monitors the packets on the interfaces
func monitorPackets(ipToMonitor net.IP, vlanIDToMonitor int, iface eniConfig) error {
	handle, err := pcap.OpenLive(iface.name, snapshotLen, promiscuous, timeout)
	if err != nil {
		return err
	}
	defer handle.Close()
	var srcPacketsProcessed int
	var dstPacketsProcessed int

	// Use the handle as a packet source to process all packets
	packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
	for packet := range packetSource.Packets() {
		fmt.Printf("packet: %v\n", packet)

		network := packet.Layer(layers.LayerTypeIPv4)
		if network != nil {
			srcIP := net.ParseIP(packet.NetworkLayer().NetworkFlow().Src().String())
			dstIP := net.ParseIP(packet.NetworkLayer().NetworkFlow().Dst().String())

			if iface.shouldCheckSrc && srcIP.Equal(ipToMonitor) && dstIP.Equal(ipToMonitor) {
				fmt.Printf("Src/Dst is different. Src %s Dst %s\n", packet.NetworkLayer().NetworkFlow().Src(),
					packet.NetworkLayer().NetworkFlow().Dst())
				return errors.New("SRC/Dst is different")
			}

			// Verify vlan tag (on ENIs we could see other IP pkts as well)
			if srcIP.Equal(ipToMonitor) || dstIP.Equal(ipToMonitor) {
				if iface.shouldVerifyVlanTag {

					dot1QPkt := packet.Layer(layers.LayerTypeDot1Q)
					if dot1QPkt == nil {
						fmt.Printf("vlan packet not found when expected.\n")
						return errors.New("vlan packet not found when expected")
					}

					dot1Q, _ := dot1QPkt.(*layers.Dot1Q)
					if dot1Q.VLANIdentifier != uint16(vlanIDToMonitor) {
						fmt.Printf("VlanIDs are different. expected %d but found %d\n", vlanIDToMonitor, dot1Q.VLANIdentifier)
						return errors.New("vlanIDs are different")
					}
				}

				/*icmpPkt := packet.Layer(layers.LayerTypeICMPv4)
				if icmpPkt != nil {
					icmpData, _ := icmpPkt.(*layers.ICMPv4)
					log.Infof("Icmp packet sequence: %d", icmpData.Seq)
				}*/

				if srcIP.Equal(ipToMonitor) {
					fmt.Printf("Source pkt is verified on the iface %s\n", iface.name)
					srcPacketsProcessed++
				} else {
					fmt.Printf("Dst pkt is verified on the iface %s\n", iface.name)
					dstPacketsProcessed++
				}

				if srcPacketsProcessed >= 5 && dstPacketsProcessed >= 5 {
					break
				}
			}
		}
	}
	return nil
}

// printVersion prints the binary version to stderr.
func printVersion() {
	fmt.Fprintf(os.Stderr, "%s v%s\n", shortDescription, version)
}

// printUsage prints usage information to stderr.
func printUsage() {
	printVersion()
	fmt.Fprintln(os.Stderr, longDescription+"\n")
	flag.PrintDefaults()
}
