// Copyright 2022 CNI 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.

// This is a sample chained plugin that supports multiple CNI versions. It
// parses prevResult according to the cniVersion
package main

import (
	"fmt"

	"github.com/coreos/go-iptables/iptables"

	types100 "github.com/containernetworking/cni/pkg/types/100"
	"github.com/containernetworking/plugins/pkg/utils"
)

func setupIngressPolicy(conf *FirewallNetConf, prevResult *types100.Result) error {
	switch conf.IngressPolicy {
	case "", IngressPolicyOpen:
		// NOP
		return nil
	case IngressPolicySameBridge:
		return setupIngressPolicyBridgeIsolation(conf, prevResult, false)
	case IngressPolicyIsolated:
		return setupIngressPolicyBridgeIsolation(conf, prevResult, true)
	default:
		return fmt.Errorf("unknown ingress policy: %q", conf.IngressPolicy)
	}
}

func setupIngressPolicyBridgeIsolation(conf *FirewallNetConf, prevResult *types100.Result, isolated bool) error {
	if len(prevResult.Interfaces) == 0 {
		return fmt.Errorf("interface needs to be set for ingress policy %q, make sure to chain \"firewall\" plugin with \"bridge\"",
			conf.IngressPolicy)
	}
	intf := prevResult.Interfaces[0]
	if intf == nil {
		return fmt.Errorf("got nil interface")
	}
	bridgeName := intf.Name
	if bridgeName == "" {
		return fmt.Errorf("got empty bridge name")
	}
	for _, iptProto := range findProtos(conf) {
		ipt, err := iptables.NewWithProtocol(iptProto)
		if err != nil {
			return err
		}
		if err := setupIsolationChains(ipt, bridgeName, isolated); err != nil {
			return err
		}
	}
	return nil
}

func teardownIngressPolicy(conf *FirewallNetConf) error {
	switch conf.IngressPolicy {
	case "", IngressPolicyOpen:
		// NOP
		return nil
	case IngressPolicySameBridge, IngressPolicyIsolated:
		// NOP
		//
		// We can't be sure whether conf.bridgeName is still in use by other containers.
		// So we do not remove the iptable rules that are created per bridge.
		return nil
	default:
		return fmt.Errorf("unknown ingress policy: %q", conf.IngressPolicy)
	}
}

const (
	filterTableName  = "filter"  // built-in
	forwardChainName = "FORWARD" // built-in
)

// setupIsolationChains executes the following iptables commands for isolating networks:
// ```
// iptables -N CNI-ISOLATION-STAGE-1
// iptables -N CNI-ISOLATION-STAGE-2
// # NOTE: "-j CNI-ISOLATION-STAGE-1" needs to be before "CNI-FORWARD" chain. So we use -I here.
// iptables -I FORWARD -j CNI-ISOLATION-STAGE-1
// iptables -A CNI-ISOLATION-STAGE-1 -i ${bridgeName} ! -o ${bridgeName} -j CNI-ISOLATION-STAGE-2
// [isolated=true] iptables -A CNI-ISOLATION-STAGE-1 -i ${bridgeName} -o ${bridgeName} -j DROP
// iptables -A CNI-ISOLATION-STAGE-1 -j RETURN
// iptables -A CNI-ISOLATION-STAGE-2 -o ${bridgeName} -j DROP
// iptables -A CNI-ISOLATION-STAGE-2 -j RETURN
// ```
func setupIsolationChains(ipt *iptables.IPTables, bridgeName string, isolated bool) error {
	const (
		// Future version may support custom chain names
		stage1Chain = "CNI-ISOLATION-STAGE-1"
		stage2Chain = "CNI-ISOLATION-STAGE-2"
	)

	ingressPolicyName := "same-bridge"
	if isolated {
		ingressPolicyName = "isolated"
	}

	withPolicyComment := withPolicyComment(ingressPolicyName)
	// Commands:
	// ```
	// iptables -N CNI-ISOLATION-STAGE-1
	// iptables -N CNI-ISOLATION-STAGE-2
	// ```
	for _, chain := range []string{stage1Chain, stage2Chain} {
		if err := utils.EnsureChain(ipt, filterTableName, chain); err != nil {
			return err
		}
	}

	// Commands:
	// ```
	// iptables -I FORWARD -j CNI-ISOLATION-STAGE-1
	// ```
	jumpToStage1 := withDefaultComment([]string{"-j", stage1Chain})
	//  NOTE: "-j CNI-ISOLATION-STAGE-1" needs to be before "CNI-FORWARD" created by CNI firewall plugin.
	// So we specify prepend = true .
	const jumpToStage1Prepend = true
	if err := utils.InsertUnique(ipt, filterTableName, forwardChainName, jumpToStage1Prepend, jumpToStage1); err != nil {
		return err
	}

	// Commands:
	// ```
	// iptables -A CNI-ISOLATION-STAGE-1 -i ${bridgeName} ! -o ${bridgeName} -j CNI-ISOLATION-STAGE-2
	// [isolate=true] iptables -A CNI-ISOLATION-STAGE-1 -i ${bridgeName} -o ${bridgeName} -j DROP
	// iptables -A CNI-ISOLATION-STAGE-1 -j RETURN
	// ```
	stage1BridgeRule := withPolicyComment(isolationStage1BridgeRule(bridgeName, stage2Chain))
	stage1BridgeDropRule := withPolicyComment(isolationStage1BridgeDropRule(bridgeName))
	// prepend = true because this needs to be before "-j RETURN"
	const stage1BridgePrepend = true
	if err := utils.InsertUnique(ipt, filterTableName, stage1Chain, stage1BridgePrepend, stage1BridgeRule); err != nil {
		return err
	}
	if isolated {
		if err := utils.InsertUnique(ipt, filterTableName, stage1Chain, stage1BridgePrepend, stage1BridgeDropRule); err != nil {
			return err
		}
	}

	stage1Return := withDefaultComment([]string{"-j", "RETURN"})
	if err := utils.InsertUnique(ipt, filterTableName, stage1Chain, false, stage1Return); err != nil {
		return err
	}

	// Commands:
	// ```
	// iptables -A CNI-ISOLATION-STAGE-2 -o ${bridgeName} -j DROP
	// iptables -A CNI-ISOLATION-STAGE-2 -j RETURN
	// ```
	stage2Bridge := withDefaultComment(isolationStage2BridgeRule(bridgeName))
	// prepend = true because this needs to be before "-j RETURN"
	const stage2BridgePrepend = true
	if err := utils.InsertUnique(ipt, filterTableName, stage2Chain, stage2BridgePrepend, stage2Bridge); err != nil {
		return err
	}
	stage2Return := withDefaultComment([]string{"-j", "RETURN"})
	return utils.InsertUnique(ipt, filterTableName, stage2Chain, false, stage2Return)
}

func isolationStage1BridgeRule(bridgeName, stage2Chain string) []string {
	return []string{"-i", bridgeName, "!", "-o", bridgeName, "-j", stage2Chain}
}

func isolationStage1BridgeDropRule(bridgeName string) []string {
	return []string{"-i", bridgeName, "-o", bridgeName, "-j", "DROP"}
}

func isolationStage2BridgeRule(bridgeName string) []string {
	return []string{"-o", bridgeName, "-j", "DROP"}
}

func withPolicyComment(policy string) func([]string) []string {
	return func(rule []string) []string {
		comment := fmt.Sprintf("CNI firewall plugin rules (ingressPolicy: %s)", policy)

		return withComment(rule, comment)
	}
}

func withDefaultComment(rule []string) []string {
	defaultComment := "CNI firewall plugin rules (ingressPolicy: same-bridge|isolated)"
	return withComment(rule, defaultComment)
}

func withComment(rule []string, comment string) []string {
	return append(rule, []string{"-m", "comment", "--comment", comment}...)
}
