//+build linux

package netlink

import (
	"errors"
	"fmt"
	"os"

	"golang.org/x/sys/unix"
)

// errNetNSDisabled is returned when network namespaces are unavailable on
// a given system.
var errNetNSDisabled = errors.New("netlink: network namespaces are not enabled on this system")

// A netNS is a handle that can manipulate network namespaces.
//
// Operations performed on a netNS must use runtime.LockOSThread before
// manipulating any network namespaces.
type netNS struct {
	// The handle to a network namespace.
	f *os.File

	// Indicates if network namespaces are disabled on this system, and thus
	// operations should become a no-op or return errors.
	disabled bool
}

// threadNetNS constructs a netNS using the network namespace of the calling
// thread. If the namespace is not the default namespace, runtime.LockOSThread
// should be invoked first.
func threadNetNS() (*netNS, error) {
	return fileNetNS(fmt.Sprintf("/proc/self/task/%d/ns/net", unix.Gettid()))
}

// fileNetNS opens file and creates a netNS. fileNetNS should only be called
// directly in tests.
func fileNetNS(file string) (*netNS, error) {
	f, err := os.Open(file)
	switch {
	case err == nil:
		return &netNS{f: f}, nil
	case os.IsNotExist(err):
		// Network namespaces are not enabled on this system. Use this signal
		// to return errors elsewhere if the caller explicitly asks for a
		// network namespace to be set.
		return &netNS{disabled: true}, nil
	default:
		return nil, err
	}
}

// Close releases the handle to a network namespace.
func (n *netNS) Close() error {
	return n.do(func() error {
		return n.f.Close()
	})
}

// FD returns a file descriptor which represents the network namespace.
func (n *netNS) FD() int {
	if n.disabled {
		// No reasonable file descriptor value in this case, so specify a
		// non-existent one.
		return -1
	}

	return int(n.f.Fd())
}

// Restore restores the original network namespace for the calling thread.
func (n *netNS) Restore() error {
	return n.do(func() error {
		return n.Set(n.FD())
	})
}

// Set sets a new network namespace for the current thread using fd.
func (n *netNS) Set(fd int) error {
	return n.do(func() error {
		return os.NewSyscallError("setns", unix.Setns(fd, unix.CLONE_NEWNET))
	})
}

// do runs fn if network namespaces are enabled on this system.
func (n *netNS) do(fn func() error) error {
	if n.disabled {
		return errNetNSDisabled
	}

	return fn()
}
