// Copyright 2015-2016 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 iptables

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"

	"github.com/aws/amazon-ecs-init/ecs-init/exec"
	log "github.com/cihub/seelog"
	"github.com/pkg/errors"
)

// iptablesAction enumerates different actions for the iptables command
type iptablesAction string

const (
	iptablesExecutable            = "iptables"
	credentialsProxyIpAddress     = "169.254.170.2"
	credentialsProxyPort          = "80"
	localhostIpAddress            = "127.0.0.1"
	localhostCredentialsProxyPort = "51679"
	localhostNetwork              = "127.0.0.0/8"
	// iptablesAppend enumerates the 'append' action
	iptablesAppend iptablesAction = "-A"
	// iptablesInsert enumerates the 'insert' action
	iptablesInsert iptablesAction = "-I"
	// iptablesDelete enumerates the 'delete' action
	iptablesDelete iptablesAction = "-D"

	iptablesTableFilter = "filter"
	iptablesTableNat    = "nat"

	offhostIntrospectionAccessConfigEnv   = "ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS"
	offhostIntrospectonAccessInterfaceEnv = "ECS_OFFHOST_INTROSPECTION_INTERFACE_NAME"
	agentIntrospectionServerPort          = "51678"

	ipv4RouteFile                         = "/proc/net/route"
	ipv4ZeroAddrInHex                     = "00000000"
	loopbackInterfaceName                 = "lo"
	fallbackOffhostIntrospectionInterface = "eth0"
)

var (
	defaultOffhostIntrospectionInterface = ""
)

// NetfilterRoute implements the engine.credentialsProxyRoute interface by
// running the external 'iptables' command
type NetfilterRoute struct {
	cmdExec exec.Exec
}

// getNetfilterChainArgsFunc defines a function pointer type that returns
// a slice of arguments for modifying a netfilter chain
type getNetfilterChainArgsFunc func() []string

// NewNetfilterRoute creates a new NetfilterRoute object
func NewNetfilterRoute(cmdExec exec.Exec) (*NetfilterRoute, error) {
	// Return an error if 'iptables' command cannot be found in the path
	_, err := cmdExec.LookPath(iptablesExecutable)
	if err != nil {
		log.Errorf("Error searching '%s' executable: %v", iptablesExecutable, err)
		return nil, err
	}

	defaultOffhostIntrospectionInterface, err = getOffhostIntrospectionInterface()
	if err != nil {
		log.Warnf("Error resolving default offhost introspection network interface, will use eth0 as fallback: %+v", err)
		// fall back to the previous behavior (always use 'eth0') in the rare case that it
		// might affect some customer with a special routing setup that's previously working.
		defaultOffhostIntrospectionInterface = fallbackOffhostIntrospectionInterface
	}

	return &NetfilterRoute{
		cmdExec: cmdExec,
	}, nil
}

// Create creates the credentials proxy endpoint route in the netfilter table
func (route *NetfilterRoute) Create() error {
	err := route.modifyNetfilterEntry(iptablesTableNat, iptablesAppend, getPreroutingChainArgs)
	if err != nil {
		return err
	}

	if !skipLocalhostTrafficFilter() {
		err = route.modifyNetfilterEntry(iptablesTableFilter, iptablesInsert, getLocalhostTrafficFilterInputChainArgs)
		if err != nil {
			return err
		}
	}

	if !allowOffhostIntrospection() {
		err = route.modifyNetfilterEntry(iptablesTableFilter, iptablesInsert, getBlockIntrospectionOffhostAccessInputChainArgs)
		if err != nil {
			log.Errorf("Error adding input chain entry to block offhost introspection access: %v", err)
		}
	}

	return route.modifyNetfilterEntry(iptablesTableNat, iptablesAppend, getOutputChainArgs)
}

// Remove removes the route for the credentials endpoint from the netfilter
// table
func (route *NetfilterRoute) Remove() error {
	preroutingErr := route.modifyNetfilterEntry(iptablesTableNat, iptablesDelete, getPreroutingChainArgs)
	if preroutingErr != nil {
		// Add more context for error in modifying the prerouting chain
		preroutingErr = fmt.Errorf("error removing prerouting chain entry: %v", preroutingErr)
	}

	var localhostInputError, introspectionInputError error
	if !skipLocalhostTrafficFilter() {
		localhostInputError = route.modifyNetfilterEntry(iptablesTableFilter, iptablesDelete, getLocalhostTrafficFilterInputChainArgs)
		if localhostInputError != nil {
			localhostInputError = fmt.Errorf("error removing input chain entry: %v", localhostInputError)
		}
	}

	introspectionInputError = route.modifyNetfilterEntry(iptablesTableFilter, iptablesDelete, getBlockIntrospectionOffhostAccessInputChainArgs)
	if introspectionInputError != nil {
		introspectionInputError = fmt.Errorf("error removing input chain entry: %v", introspectionInputError)
	}

	outputErr := route.modifyNetfilterEntry(iptablesTableNat, iptablesDelete, getOutputChainArgs)
	if outputErr != nil {
		// Add more context for error in modifying the output chain
		outputErr = fmt.Errorf("error removing output chain entry: %v", outputErr)
	}

	return combinedError(preroutingErr, localhostInputError, introspectionInputError, outputErr)
}

func combinedError(errs ...error) error {
	errMsgs := []string{}
	for _, err := range errs {
		if err != nil {
			errMsgs = append(errMsgs, err.Error())
		}
	}

	if len(errMsgs) == 0 {
		return nil
	}

	return errors.New(strings.Join(errMsgs, ";"))
}

// modifyNetfilterEntry modifies an entry in the netfilter table based on
// the action and the function pointer to get arguments for modifying the
// chain
func (route *NetfilterRoute) modifyNetfilterEntry(table string, action iptablesAction, getNetfilterChainArgs getNetfilterChainArgsFunc) error {
	args := append(getTableArgs(table), string(action))
	args = append(args, getNetfilterChainArgs()...)
	cmd := route.cmdExec.Command(iptablesExecutable, args...)
	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Errorf("Error performing action '%s' for iptables route: %v; raw output: %s", getActionName(action), err, out)
	}

	return err
}

func getTableArgs(table string) []string {
	return []string{"-t", table}
}

func getPreroutingChainArgs() []string {
	return []string{
		"PREROUTING",
		"-p", "tcp",
		"-d", credentialsProxyIpAddress,
		"--dport", credentialsProxyPort,
		"-j", "DNAT",
		"--to-destination", localhostIpAddress + ":" + localhostCredentialsProxyPort,
	}
}

func getLocalhostTrafficFilterInputChainArgs() []string {
	return []string{
		"INPUT",
		"--dst", localhostNetwork,
		"!", "--src", localhostNetwork,
		"-m", "conntrack",
		"!", "--ctstate", "RELATED,ESTABLISHED,DNAT",
		"-j", "DROP",
	}
}

func getBlockIntrospectionOffhostAccessInputChainArgs() []string {
	return []string{
		"INPUT",
		"-p", "tcp",
		"-i", defaultOffhostIntrospectionInterface,
		"--dport", agentIntrospectionServerPort,
		"-j", "DROP",
	}
}

func getOffhostIntrospectionInterface() (string, error) {
	s := os.Getenv(offhostIntrospectonAccessInterfaceEnv)
	if s != "" {
		return s, nil
	}
	return getDefaultNetworkInterfaceIPv4()
}

// Parse /proc/net/route file and retrieves a non-loopback default network interface for IPv4 (which maps to default 0.0.0.0/0 destination)
// Example file content:
// $ sudo cat /proc/net/route
// Iface   Destination Gateway     Flags   RefCnt  Use Metric  Mask   MTU Window  IRTT
// ens5    00000000    01201FAC    0003    0   		0   512 00000000    0   0   	0
// ...
//
// 1st column contains interface name
// 2nd column contains destination network in hex
var getDefaultNetworkInterfaceIPv4 = func() (string, error) {
	input, err := os.Open(ipv4RouteFile)
	if err != nil {
		return "", fmt.Errorf("could not get IPv4 route input: %v", err)
	}
	defer input.Close()
	return scanIPv4RoutesForDefaultInterface(input)
}

func scanIPv4RoutesForDefaultInterface(input io.Reader) (string, error) {
	scanner := bufio.NewScanner(input)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "Iface") { // skip header line
			continue
		}
		fields := strings.Fields(line)
		if (fields[1] == ipv4ZeroAddrInHex) && (fields[0] != loopbackInterfaceName) {
			return fields[0], nil
		}
	}
	return "", fmt.Errorf("could not find a default IPv4 route through non-loopback interface")
}

func getOutputChainArgs() []string {
	return []string{
		"OUTPUT",
		"-p", "tcp",
		"-d", credentialsProxyIpAddress,
		"--dport", credentialsProxyPort,
		"-j", "REDIRECT",
		"--to-ports", localhostCredentialsProxyPort,
	}
}

func getActionName(action iptablesAction) string {
	switch action {
	case iptablesAppend:
		return "append"
	case iptablesInsert:
		return "insert"
	default:
		return "delete"
	}
}

func skipLocalhostTrafficFilter() bool {
	s := os.Getenv("ECS_SKIP_LOCALHOST_TRAFFIC_FILTER")
	if s == "" {
		return false
	}
	b, err := strconv.ParseBool(s)
	if err != nil {
		log.Errorf("Failed to parse value for ECS_SKIP_LOCALHOST_TRAFFIC_FILTER [%s]: %v. Default it to false.", s, err)
		return false
	}
	return b
}

func allowOffhostIntrospection() bool {
	s := os.Getenv(offhostIntrospectionAccessConfigEnv)
	if s == "" {
		return false
	}
	b, err := strconv.ParseBool(s)
	if err != nil {
		log.Errorf("Failed to parse value for %s [%s]: %v. Default it to false.",
			offhostIntrospectionAccessConfigEnv, s, err)
		return false
	}
	return b
}
