// Copyright 2016 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 "meta-plugin". It reads in its own netconf, it does not create
// any network interface but just changes the network sysctl.

package main

import (
	"fmt"
	"net"

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

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

func getPrivChainRules(ip string) [][]string {
	var rules [][]string
	rules = append(rules, []string{"-d", ip, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"})
	rules = append(rules, []string{"-s", ip, "-j", "ACCEPT"})
	return rules
}

func generateFilterRule(privChainName string) []string {
	return []string{"-m", "comment", "--comment", "CNI firewall plugin rules", "-j", privChainName}
}

func generateAdminRule(adminChainName string) []string {
	return []string{"-m", "comment", "--comment", "CNI firewall plugin admin overrides", "-j", adminChainName}
}

func cleanupRules(ipt *iptables.IPTables, privChainName string, rules [][]string) {
	for _, rule := range rules {
		ipt.Delete("filter", privChainName, rule...)
	}
}

func ensureFirstChainRule(ipt *iptables.IPTables, chain string, rule []string) error {
	exists, err := ipt.Exists("filter", chain, rule...)
	if !exists && err == nil {
		err = ipt.Insert("filter", chain, 1, rule...)
	}
	return err
}

func (ib *iptablesBackend) setupChains(ipt *iptables.IPTables) error {
	privRule := generateFilterRule(ib.privChainName)
	adminRule := generateAdminRule(ib.adminChainName)

	// Ensure our private chains exist
	if err := utils.EnsureChain(ipt, "filter", ib.privChainName); err != nil {
		return err
	}
	if err := utils.EnsureChain(ipt, "filter", ib.adminChainName); err != nil {
		return err
	}

	// Ensure our filter rule exists in the forward chain
	if err := ensureFirstChainRule(ipt, "FORWARD", privRule); err != nil {
		return err
	}

	// Ensure our admin override chain rule exists in our private chain
	return ensureFirstChainRule(ipt, ib.privChainName, adminRule)
}

func protoForIP(ip net.IPNet) iptables.Protocol {
	if ip.IP.To4() != nil {
		return iptables.ProtocolIPv4
	}
	return iptables.ProtocolIPv6
}

func (ib *iptablesBackend) addRules(_ *FirewallNetConf, result *current.Result, ipt *iptables.IPTables, proto iptables.Protocol) error {
	rules := make([][]string, 0)
	for _, ip := range result.IPs {
		if protoForIP(ip.Address) == proto {
			rules = append(rules, getPrivChainRules(ipString(ip.Address))...)
		}
	}

	if len(rules) > 0 {
		if err := ib.setupChains(ipt); err != nil {
			return err
		}

		// Clean up on any errors
		var err error
		defer func() {
			if err != nil {
				cleanupRules(ipt, ib.privChainName, rules)
			}
		}()

		for _, rule := range rules {
			err = ipt.AppendUnique("filter", ib.privChainName, rule...)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

func (ib *iptablesBackend) delRules(_ *FirewallNetConf, result *current.Result, ipt *iptables.IPTables, proto iptables.Protocol) {
	rules := make([][]string, 0)
	for _, ip := range result.IPs {
		if protoForIP(ip.Address) == proto {
			rules = append(rules, getPrivChainRules(ipString(ip.Address))...)
		}
	}
	if len(rules) > 0 {
		cleanupRules(ipt, ib.privChainName, rules)
	}
}

func (ib *iptablesBackend) checkRules(_ *FirewallNetConf, result *current.Result, ipt *iptables.IPTables, proto iptables.Protocol) error {
	rules := make([][]string, 0)
	for _, ip := range result.IPs {
		if protoForIP(ip.Address) == proto {
			rules = append(rules, getPrivChainRules(ipString(ip.Address))...)
		}
	}

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

	// Ensure our private chains exist
	if err := utils.EnsureChain(ipt, "filter", ib.privChainName); err != nil {
		return err
	}
	if err := utils.EnsureChain(ipt, "filter", ib.adminChainName); err != nil {
		return err
	}

	// Ensure our filter rule exists in the forward chain
	privRule := generateFilterRule(ib.privChainName)
	privExists, err := ipt.Exists("filter", "FORWARD", privRule...)
	if err != nil {
		return err
	}
	if !privExists {
		return fmt.Errorf("expected %v rule %v not found", "FORWARD", privRule)
	}

	// Ensure our admin override chain rule exists in our private chain
	adminRule := generateAdminRule(ib.adminChainName)
	adminExists, err := ipt.Exists("filter", ib.privChainName, adminRule...)
	if err != nil {
		return err
	}
	if !adminExists {
		return fmt.Errorf("expected %v rule %v not found", ib.privChainName, adminRule)
	}

	// ensure rules for this IP address exist
	for _, rule := range rules {
		// Ensure our rule exists in our private chain
		exists, err := ipt.Exists("filter", ib.privChainName, rule...)
		if err != nil {
			return err
		}
		if !exists {
			return fmt.Errorf("expected rule %v not found", rule)
		}
	}

	return nil
}

func findProtos(conf *FirewallNetConf) []iptables.Protocol {
	protos := []iptables.Protocol{iptables.ProtocolIPv4, iptables.ProtocolIPv6}
	if conf.PrevResult != nil {
		// If PrevResult is given, scan all IP addresses to figure out
		// which IP versions to use
		protos = []iptables.Protocol{}
		result, _ := current.NewResultFromResult(conf.PrevResult)
		for _, addr := range result.IPs {
			if addr.Address.IP.To4() != nil {
				protos = append(protos, iptables.ProtocolIPv4)
			} else {
				protos = append(protos, iptables.ProtocolIPv6)
			}
		}
	}
	return protos
}

type iptablesBackend struct {
	protos         map[iptables.Protocol]*iptables.IPTables
	privChainName  string
	adminChainName string
}

// iptablesBackend implements the FirewallBackend interface
var _ FirewallBackend = &iptablesBackend{}

func newIptablesBackend(conf *FirewallNetConf) (FirewallBackend, error) {
	adminChainName := conf.IptablesAdminChainName
	if adminChainName == "" {
		adminChainName = "CNI-ADMIN"
	}

	backend := &iptablesBackend{
		privChainName:  "CNI-FORWARD",
		adminChainName: adminChainName,
		protos:         make(map[iptables.Protocol]*iptables.IPTables),
	}

	for _, proto := range []iptables.Protocol{iptables.ProtocolIPv4, iptables.ProtocolIPv6} {
		ipt, err := iptables.NewWithProtocol(proto)
		if err != nil {
			return nil, fmt.Errorf("could not initialize iptables protocol %v: %v", proto, err)
		}
		backend.protos[proto] = ipt
	}

	return backend, nil
}

func (ib *iptablesBackend) Add(conf *FirewallNetConf, result *current.Result) error {
	for proto, ipt := range ib.protos {
		if err := ib.addRules(conf, result, ipt, proto); err != nil {
			return err
		}
	}
	return nil
}

func (ib *iptablesBackend) Del(conf *FirewallNetConf, result *current.Result) error {
	for proto, ipt := range ib.protos {
		ib.delRules(conf, result, ipt, proto)
	}
	return nil
}

func (ib *iptablesBackend) Check(conf *FirewallNetConf, result *current.Result) error {
	for proto, ipt := range ib.protos {
		if err := ib.checkRules(conf, result, ipt, proto); err != nil {
			return err
		}
	}
	return nil
}
