//go:build !remote

package libpod

import (
	"fmt"
	"net"

	"github.com/containernetworking/plugins/pkg/ns"
	"github.com/containers/common/libnetwork/types"
	"github.com/containers/common/pkg/netns"
	"github.com/containers/podman/v5/libpod/define"
	"github.com/containers/podman/v5/pkg/rootless"
	"github.com/opencontainers/runtime-spec/specs-go"
	"github.com/sirupsen/logrus"
	"github.com/vishvananda/netlink"
)

// Create and configure a new network namespace for a container
func (r *Runtime) configureNetNS(ctr *Container, ctrNS string) (status map[string]types.StatusBlock, rerr error) {
	if err := r.exposeMachinePorts(ctr.config.PortMappings); err != nil {
		return nil, err
	}
	defer func() {
		// make sure to unexpose the gvproxy ports when an error happens
		if rerr != nil {
			if err := r.unexposeMachinePorts(ctr.config.PortMappings); err != nil {
				logrus.Errorf("failed to free gvproxy machine ports: %v", err)
			}
		}
	}()
	if ctr.config.NetMode.IsSlirp4netns() {
		return nil, r.setupSlirp4netns(ctr, ctrNS)
	}
	if ctr.config.NetMode.IsPasta() {
		return nil, r.setupPasta(ctr, ctrNS)
	}
	networks, err := ctr.networks()
	if err != nil {
		return nil, err
	}
	// All networks have been removed from the container.
	// This is effectively forcing net=none.
	if len(networks) == 0 {
		return nil, nil
	}

	netOpts := ctr.getNetworkOptions(networks)
	netStatus, err := r.setUpNetwork(ctrNS, netOpts)
	if err != nil {
		return nil, err
	}
	defer func() {
		// do not forget to tear down the netns when a later error happened.
		if rerr != nil {
			if err := r.teardownNetworkBackend(ctrNS, netOpts); err != nil {
				logrus.Warnf("failed to teardown network after failed setup: %v", err)
			}
		}
	}()

	// set up rootless port forwarder when rootless with ports and the network status is empty,
	// if this is called from network reload the network status will not be empty and we should
	// not set up port because they are still active
	if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 && ctr.getNetworkStatus() == nil {
		// set up port forwarder for rootless netns
		// TODO: support slirp4netns port forwarder as well
		// make sure to fix this in container.handleRestartPolicy() as well
		// Important we have to call this after r.setUpNetwork() so that
		// we can use the proper netStatus
		err = r.setupRootlessPortMappingViaRLK(ctr, ctrNS, netStatus)
	}
	return netStatus, err
}

// Create and configure a new network namespace for a container
func (r *Runtime) createNetNS(ctr *Container) (n string, q map[string]types.StatusBlock, retErr error) {
	ctrNS, err := netns.NewNS()
	if err != nil {
		return "", nil, fmt.Errorf("creating network namespace for container %s: %w", ctr.ID(), err)
	}
	defer func() {
		if retErr != nil {
			if err := netns.UnmountNS(ctrNS.Path()); err != nil {
				logrus.Errorf("Unmounting partially created network namespace for container %s: %v", ctr.ID(), err)
			}
			if err := ctrNS.Close(); err != nil {
				logrus.Errorf("Closing partially created network namespace for container %s: %v", ctr.ID(), err)
			}
		}
	}()

	logrus.Debugf("Made network namespace at %s for container %s", ctrNS.Path(), ctr.ID())

	var networkStatus map[string]types.StatusBlock
	networkStatus, err = r.configureNetNS(ctr, ctrNS.Path())
	return ctrNS.Path(), networkStatus, err
}

// Configure the network namespace using the container process
func (r *Runtime) setupNetNS(ctr *Container) error {
	nsProcess := fmt.Sprintf("/proc/%d/ns/net", ctr.state.PID)
	nsPath, err := netns.NewNSFrom(nsProcess)
	if err != nil {
		return err
	}

	networkStatus, err := r.configureNetNS(ctr, nsPath)

	// Assign NetNS attributes to container
	ctr.state.NetNS = nsPath
	ctr.state.NetworkStatus = networkStatus
	return err
}

// Tear down a network namespace, undoing all state associated with it.
func (r *Runtime) teardownNetNS(ctr *Container) error {
	if err := r.unexposeMachinePorts(ctr.config.PortMappings); err != nil {
		// do not return an error otherwise we would prevent network cleanup
		logrus.Errorf("failed to free gvproxy machine ports: %v", err)
	}

	// Do not check the error here, we want to always umount the netns
	// This will ensure that the container interface will be deleted
	// even when there is a CNI or netavark bug.
	prevErr := r.teardownNetwork(ctr)

	// First unmount the namespace
	if err := netns.UnmountNS(ctr.state.NetNS); err != nil {
		if prevErr != nil {
			logrus.Error(prevErr)
		}
		return fmt.Errorf("unmounting network namespace for container %s: %w", ctr.ID(), err)
	}

	ctr.state.NetNS = ""

	return prevErr
}

func getContainerNetNS(ctr *Container) (string, *Container, error) {
	if ctr.state.NetNS != "" {
		return ctr.state.NetNS, nil, nil
	}
	if ctr.config.NetNsCtr != "" {
		c, err := ctr.runtime.GetContainer(ctr.config.NetNsCtr)
		if err != nil {
			return "", nil, err
		}
		if err = c.syncContainer(); err != nil {
			return "", c, err
		}
		netNs, c2, err := getContainerNetNS(c)
		if c2 != nil {
			c = c2
		}
		return netNs, c, err
	}
	return "", nil, nil
}

// Returns a map of interface name to statistics for that interface.
func getContainerNetIO(ctr *Container) (map[string]define.ContainerNetworkStats, error) {
	perNetworkStats := make(map[string]define.ContainerNetworkStats)

	netNSPath, _, netPathErr := getContainerNetNS(ctr)
	if netPathErr != nil {
		return nil, netPathErr
	}
	if netNSPath == "" {
		// If netNSPath is empty, it was set as none, and no netNS was set up
		// this is a valid state and thus return no error, nor any statistics
		return nil, nil
	}

	err := ns.WithNetNSPath(netNSPath, func(_ ns.NetNS) error {
		links, err := netlink.LinkList()
		if err != nil {
			return fmt.Errorf("retrieving all network interfaces: %w", err)
		}
		for _, link := range links {
			attributes := link.Attrs()
			if attributes.Flags&net.FlagLoopback != 0 {
				continue
			}

			if attributes.Statistics != nil {
				perNetworkStats[attributes.Name] = getNetStatsFromNetlinkStats(attributes.Statistics)
			}
		}
		return nil
	})
	return perNetworkStats, err
}

func getNetStatsFromNetlinkStats(stats *netlink.LinkStatistics) define.ContainerNetworkStats {
	return define.ContainerNetworkStats{
		RxBytes:   stats.RxBytes,
		RxDropped: stats.RxDropped,
		RxErrors:  stats.RxErrors,
		RxPackets: stats.RxPackets,
		TxBytes:   stats.TxBytes,
		TxDropped: stats.TxDropped,
		TxErrors:  stats.TxErrors,
		TxPackets: stats.TxPackets,
	}
}

// joinedNetworkNSPath returns netns path and bool if netns was set
func (c *Container) joinedNetworkNSPath() (string, bool) {
	for _, namespace := range c.config.Spec.Linux.Namespaces {
		if namespace.Type == specs.NetworkNamespace {
			return namespace.Path, true
		}
	}
	return "", false
}

func (c *Container) inspectJoinedNetworkNS(networkns string) (q types.StatusBlock, retErr error) {
	var result types.StatusBlock
	err := ns.WithNetNSPath(networkns, func(_ ns.NetNS) error {
		ifaces, err := net.Interfaces()
		if err != nil {
			return err
		}
		routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL)
		if err != nil {
			return err
		}
		var gateway net.IP
		for _, route := range routes {
			// add default gateway
			// Dst is set to 0.0.0.0/0 or ::/0 which is the default route
			if route.Dst != nil && route.Dst.IP.IsUnspecified() {
				ones, _ := route.Dst.Mask.Size()
				if ones == 0 {
					gateway = route.Gw
				}
			}
		}
		result.Interfaces = make(map[string]types.NetInterface)
		for _, iface := range ifaces {
			if iface.Flags&net.FlagLoopback != 0 {
				continue
			}
			addrs, err := iface.Addrs()
			if err != nil {
				continue
			}
			if len(addrs) == 0 {
				continue
			}
			subnets := make([]types.NetAddress, 0, len(addrs))
			for _, address := range addrs {
				if ipnet, ok := address.(*net.IPNet); ok {
					if ipnet.IP.IsLinkLocalMulticast() || ipnet.IP.IsLinkLocalUnicast() {
						continue
					}
					subnet := types.NetAddress{
						IPNet: types.IPNet{
							IPNet: *ipnet,
						},
					}
					if ipnet.Contains(gateway) {
						subnet.Gateway = gateway
					}
					subnets = append(subnets, subnet)
				}
			}
			result.Interfaces[iface.Name] = types.NetInterface{
				Subnets:    subnets,
				MacAddress: types.HardwareAddr(iface.HardwareAddr),
			}
		}
		return nil
	})
	return result, err
}