// Copyright 2019 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.

package plugin

import (
	"strings"

	"github.com/aws/amazon-vpc-cni-plugins/network/netns"
	"github.com/aws/amazon-vpc-cni-plugins/plugins/aws-appmesh/config"

	log "github.com/cihub/seelog"
	cniSkel "github.com/containernetworking/cni/pkg/skel"
	cniTypes "github.com/containernetworking/cni/pkg/types"
	"github.com/coreos/go-iptables/iptables"
)

const (
	// Names of iptables chains created for App Mesh rules.
	ingressChain = "APPMESH_INGRESS"
	egressChain  = "APPMESH_EGRESS"
	// Up to 15 ports can be specified when using iptables multiport module
	multiportLimit = 15
)

// Add is the internal implementation of CNI ADD command.
func (plugin *Plugin) Add(args *cniSkel.CmdArgs) error {
	// Parse network configuration.
	netConfig, err := config.New(args)
	if err != nil {
		log.Errorf("Failed to parse netconfig from args: %v.", err)
		return err
	}

	log.Infof("Executing ADD with netconfig: %+v.", netConfig)

	// Find the network namespace.
	log.Debugf("Searching for netns %s.", args.Netns)
	ns, err := netns.GetNetNS(args.Netns)
	if err != nil {
		log.Errorf("Failed to find netns %s: %v.", args.Netns, err)
		return err
	}

	// Add IP rules in the target network namespace.
	err = ns.Run(func() error {
		var err error
		ipProtoMap := make(map[iptables.Protocol]string)
		ipProtoMap[iptables.ProtocolIPv4] = netConfig.EgressIgnoredIPv4s
		if netConfig.EnableIPv6 {
			ipProtoMap[iptables.ProtocolIPv6] = netConfig.EgressIgnoredIPv6s
		}

		for proto, ignoredIPs := range ipProtoMap {
			err = plugin.setupIptablesRules(proto, netConfig, ignoredIPs)
			if err != nil {
				log.Errorf("Failed to set up iptables rules: %v.", err)
				return err
			}
		}

		return nil
	})

	if err != nil {
		return err
	}

	// Pass through the previous result.
	log.Infof("Writing CNI result to stdout: %+v", netConfig.PrevResult)

	return cniTypes.PrintResult(netConfig.PrevResult, netConfig.CNIVersion)
}

// Del is the internal implementation of CNI DEL command.
// CNI DEL command can be called by the orchestrator multiple times for the same interface,
// and thus must be best-effort and idempotent.
func (plugin *Plugin) Del(args *cniSkel.CmdArgs) error {
	// Parse network configuration.
	netConfig, err := config.New(args)
	if err != nil {
		log.Errorf("Failed to parse netconfig from args: %v.", err)
		return err
	}

	log.Infof("Executing DEL with netconfig: %+v.", netConfig)

	// Search for the target network namespace.
	ns, err := netns.GetNetNS(args.Netns)
	if err != nil {
		log.Errorf("Failed to find netns %s: %v.", args.Netns, err)
		return err
	}

	// Delete IP rules in the target network namespace.
	err = ns.Run(func() error {
		ipProtos := []iptables.Protocol{iptables.ProtocolIPv4}
		if netConfig.EnableIPv6 {
			ipProtos = append(ipProtos, iptables.ProtocolIPv6)
		}

		for _, proto := range ipProtos {
			err = plugin.deleteIptablesRules(proto, netConfig)
			if err != nil {
				log.Errorf("Failed to delete ip rules: %v.", err)
				return err
			}
		}

		return nil
	})

	return err
}

// setupIptablesRules sets iptables/ip6tables rules in container network namespace.
func (plugin *Plugin) setupIptablesRules(
	proto iptables.Protocol,
	config *config.NetConfig,
	egressIgnoredIPs string) error {
	// Create a new iptables object.
	iptable, err := iptables.NewWithProtocol(proto)
	if err != nil {
		return err
	}

	err = plugin.setupIngressRules(iptable, config)
	if err != nil {
		return err
	}

	err = plugin.setupEgressRules(iptable, config, egressIgnoredIPs)
	if err != nil {
		return err
	}

	return nil
}

// setupEgressRules installs iptable rules to handle egress traffic.
func (plugin *Plugin) setupEgressRules(
	iptable *iptables.IPTables,
	config *config.NetConfig,
	egressIgnoredIPs string) error {

	// Create new chains.
	err := iptable.NewChain("nat", egressChain)
	if err != nil {
		return err
	}

	// Set up for outgoing traffic.
	if config.IgnoredUID != "" {
		err = iptable.Append("nat", egressChain, "-m", "owner", "--uid-owner", config.IgnoredUID, "-j", "RETURN")
		if err != nil {
			log.Errorf("Append rule for ignoredUID failed: %v", err)
			return err
		}
	}

	if config.IgnoredGID != "" {
		err = iptable.Append("nat", egressChain, "-m", "owner", "--gid-owner", config.IgnoredGID, "-j", "RETURN")
		if err != nil {
			log.Errorf("Append rule for ignoredGID failed: %v", err)
			return err
		}
	}

	if len(config.EgressIgnoredPorts) > 0 {
		err = forEachSlice(config.EgressIgnoredPorts, multiportLimit, func(ports []string) error {
			return iptable.Append("nat", egressChain, "-p", "tcp", "-m", "multiport", "--dports", strings.Join(ports, ","), "-j", "RETURN")
		})

		if err != nil {
			log.Errorf("Append rule for egressIgnoredPorts failed: %v", err)
			return err
		}
	}

	if egressIgnoredIPs != "" {
		err = iptable.Append("nat", egressChain, "-p", "tcp", "-d", egressIgnoredIPs, "-j", "RETURN")
		if err != nil {
			log.Errorf("Append rule for egressIgnoredIPs failed: %v", err)
			return err
		}
	}

	// Redirect everything that is not ignored.
	err = iptable.Append("nat", egressChain, "-p", "tcp", "-j", "REDIRECT", "--to", config.ProxyEgressPort)
	if err != nil {
		log.Errorf("Append rule to redirect traffic to proxyEgressPort failed: %v", err)
		return err
	}

	// Apply egress chain to non local traffic.
	err = iptable.Append("nat", "OUTPUT", "-p", "tcp", "-m", "addrtype", "!", "--dst-type",
		"LOCAL", "-j", egressChain)
	if err != nil {
		log.Errorf("Append rule to jump from OUTPUT to egress chain failed: %v", err)
		return err
	}

	return nil
}

// setupIngressRules installs iptable rules to handle ingress traffic.
func (plugin *Plugin) setupIngressRules(
	iptable *iptables.IPTables,
	config *config.NetConfig) error {
	if config.ProxyIngressPort == "" || len(config.AppPorts) == 0 {
		return nil
	}

	err := iptable.NewChain("nat", ingressChain)
	if err != nil {
		return err
	}

	// Route everything arriving at the application port to proxy.
	err = forEachSlice(config.AppPorts, multiportLimit, func(ports []string) error {
		return iptable.Append("nat", ingressChain, "-p", "tcp", "-m", "multiport", "--dports", strings.Join(ports, ","),
			"-j", "REDIRECT", "--to-port", config.ProxyIngressPort)
	})
	if err != nil {
		log.Errorf("Append rule to redirect traffic to proxyIngressPort failed: %v", err)
		return err
	}

	// Apply ingress chain to everything non-local.
	err = iptable.Append("nat", "PREROUTING", "-p", "tcp", "-m", "addrtype", "!", "--src-type",
		"LOCAL", "-j", ingressChain)
	if err != nil {
		log.Errorf("Append rule to jump from PREROUTING to ingress chain failed: %v", err)
		return err
	}

	return nil
}

// deleteIptablesRules deletes iptables/ip6tables rules in container network namespace.
func (plugin *Plugin) deleteIptablesRules(
	proto iptables.Protocol,
	config *config.NetConfig) error {
	/// Create a new iptables session.
	iptable, err := iptables.NewWithProtocol(proto)
	if err != nil {
		return err
	}

	err = plugin.deleteIngressRules(iptable, config)
	if err != nil {
		return err
	}

	err = plugin.deleteEgressRules(iptable)
	if err != nil {
		return err
	}

	return nil
}

// deleteIngressRules deletes the iptable rules for ingress traffic.
func (plugin *Plugin) deleteIngressRules(
	iptable *iptables.IPTables,
	config *config.NetConfig) error {
	if config.ProxyIngressPort == "" {
		return nil
	}

	//Check if ingress chain exist before deleting
	exists, err := iptable.ChainExists("nat", ingressChain)
	if err != nil {
		return err
	}
	//return nil to make sure deleting process is idempotent
	if !exists {
		log.Warn("deleting a non-existent ingress chain: %s", ingressChain)
		return nil
	}

	// Delete ingress rule from iptables.
	err = iptable.Delete("nat", "PREROUTING", "-p", "tcp", "-m", "addrtype", "!", "--src-type",
		"LOCAL", "-j", ingressChain)

	if err != nil {
		log.Errorf("Delete the rule in PREROUTING chain failed: %v", err)
		return err
	}

	// flush and delete ingress chain.
	err = iptable.ClearChain("nat", ingressChain)
	if err != nil {
		log.Errorf("Failed to flush rules in chain[%v]: %v", ingressChain, err)
		return err
	}
	err = iptable.DeleteChain("nat", ingressChain)
	if err != nil {
		log.Errorf("Failed to delete chain[%v]: %v", ingressChain, err)
		return err
	}

	return nil
}

// deleteEgressRules deletes the iptable rules for egress traffic.
func (plugin *Plugin) deleteEgressRules(iptable *iptables.IPTables) error {

	//Check if ingress chain exist before deleting
	exists, err := iptable.ChainExists("nat", egressChain)
	if err != nil {
		return err
	}
	//return nil to make sure deleting process is idempotent
	if !exists {
		log.Warn("deleting a non-existent egress chain: %s", egressChain)
		return nil
	}

	// Delete egress rule from iptables.
	err = iptable.Delete("nat", "OUTPUT", "-p", "tcp", "-m", "addrtype", "!", "--dst-type",
		"LOCAL", "-j", egressChain)
	if err != nil {
		log.Errorf("Delete the rule in OUTPUT chain failed: %v", err)
		return err
	}

	// flush and delete egress chain.
	err = iptable.ClearChain("nat", egressChain)
	if err != nil {
		log.Errorf("Failed to flush rules in chain[%v]: %v", egressChain, err)
		return err
	}
	err = iptable.DeleteChain("nat", egressChain)
	if err != nil {
		log.Errorf("Failed to delete chain[%v]: %v", egressChain, err)
		return err
	}

	return nil
}

func forEachSlice(inputPorts []string, maximumPort int, run func([]string) error) error {
	portList := make([]string, 0)

	for i, port := range inputPorts {
		//Ignore ports after reaching multiport limit
		if i == maximumPort {
			log.Errorf("multiport limit: %d exceeded, ignoring remaining rules", maximumPort)
			break
		}
		portList = append(portList, port)
	}

	if len(portList) > 0 {
		err := run(portList)
		if err != nil {
			return err
		}
	}

	return nil
}
