//go:build linux
// +build linux

package wglinux

import (
	"errors"
	"fmt"
	"os"
	"syscall"

	"github.com/mdlayher/genetlink"
	"github.com/mdlayher/netlink"
	"github.com/mdlayher/netlink/nlenc"
	"golang.org/x/sys/unix"
	"golang.zx2c4.com/wireguard/wgctrl/internal/wginternal"
	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)

var _ wginternal.Client = &Client{}

// A Client provides access to Linux WireGuard netlink information.
type Client struct {
	c      *genetlink.Conn
	family genetlink.Family

	interfaces func() ([]string, error)
}

// New creates a new Client and returns whether or not the generic netlink
// interface is available.
func New() (*Client, bool, error) {
	c, err := genetlink.Dial(&netlink.Config{Strict: true})
	if err != nil {
		return nil, false, err
	}

	return initClient(c)
}

// initClient is the internal Client constructor used in some tests.
func initClient(c *genetlink.Conn) (*Client, bool, error) {
	f, err := c.GetFamily(unix.WG_GENL_NAME)
	if err != nil {
		_ = c.Close()

		if errors.Is(err, os.ErrNotExist) {
			// The generic netlink interface is not available.
			return nil, false, nil
		}

		return nil, false, err
	}

	return &Client{
		c:      c,
		family: f,

		// By default, gather only WireGuard interfaces using rtnetlink.
		interfaces: rtnlInterfaces,
	}, true, nil
}

// Close implements wginternal.Client.
func (c *Client) Close() error {
	return c.c.Close()
}

// Devices implements wginternal.Client.
func (c *Client) Devices() ([]*wgtypes.Device, error) {
	// By default, rtnetlink is used to fetch a list of all interfaces and then
	// filter that list to only find WireGuard interfaces.
	//
	// The remainder of this function assumes that any returned device from this
	// function is a valid WireGuard device.
	ifis, err := c.interfaces()
	if err != nil {
		return nil, err
	}

	ds := make([]*wgtypes.Device, 0, len(ifis))
	for _, ifi := range ifis {
		d, err := c.Device(ifi)
		if err != nil {
			return nil, err
		}

		ds = append(ds, d)
	}

	return ds, nil
}

// Device implements wginternal.Client.
func (c *Client) Device(name string) (*wgtypes.Device, error) {
	// Don't bother querying netlink with empty input.
	if name == "" {
		return nil, os.ErrNotExist
	}

	// Fetching a device by interface index is possible as well, but we only
	// support fetching by name as it seems to be more convenient in general.
	b, err := netlink.MarshalAttributes([]netlink.Attribute{{
		Type: unix.WGDEVICE_A_IFNAME,
		Data: nlenc.Bytes(name),
	}})
	if err != nil {
		return nil, err
	}

	msgs, err := c.execute(unix.WG_CMD_GET_DEVICE, netlink.Request|netlink.Dump, b)
	if err != nil {
		return nil, err
	}

	return parseDevice(msgs)
}

// ConfigureDevice implements wginternal.Client.
func (c *Client) ConfigureDevice(name string, cfg wgtypes.Config) error {
	// Large configurations are split into batches for use with netlink.
	for _, b := range buildBatches(cfg) {
		attrs, err := configAttrs(name, b)
		if err != nil {
			return err
		}

		// Request acknowledgement of our request from netlink, even though the
		// output messages are unused.  The netlink package checks and trims the
		// status code value.
		if _, err := c.execute(unix.WG_CMD_SET_DEVICE, netlink.Request|netlink.Acknowledge, attrs); err != nil {
			return err
		}
	}

	return nil
}

// execute executes a single WireGuard netlink request with the specified command,
// header flags, and attribute arguments.
func (c *Client) execute(command uint8, flags netlink.HeaderFlags, attrb []byte) ([]genetlink.Message, error) {
	msg := genetlink.Message{
		Header: genetlink.Header{
			Command: command,
			Version: unix.WG_GENL_VERSION,
		},
		Data: attrb,
	}

	msgs, err := c.c.Execute(msg, c.family.ID, flags)
	if err == nil {
		return msgs, nil
	}

	// We don't want to expose netlink errors directly to callers so unpack to
	// something more generic.
	oerr, ok := err.(*netlink.OpError)
	if !ok {
		// Expect all errors to conform to netlink.OpError.
		return nil, fmt.Errorf("wglinux: netlink operation returned non-netlink error (please file a bug: https://golang.zx2c4.com/wireguard/wgctrl): %v", err)
	}

	switch oerr.Err {
	// Convert "no such device" and "not a wireguard device" to an error
	// compatible with os.ErrNotExist for easy checking.
	case unix.ENODEV, unix.ENOTSUP:
		return nil, os.ErrNotExist
	default:
		// Expose the inner error directly (such as EPERM).
		return nil, oerr.Err
	}
}

// rtnlInterfaces uses rtnetlink to fetch a list of WireGuard interfaces.
func rtnlInterfaces() ([]string, error) {
	// Use the stdlib's rtnetlink helpers to get ahold of a table of all
	// interfaces, so we can begin filtering it down to just WireGuard devices.
	tab, err := syscall.NetlinkRIB(unix.RTM_GETLINK, unix.AF_UNSPEC)
	if err != nil {
		return nil, fmt.Errorf("wglinux: failed to get list of interfaces from rtnetlink: %v", err)
	}

	msgs, err := syscall.ParseNetlinkMessage(tab)
	if err != nil {
		return nil, fmt.Errorf("wglinux: failed to parse rtnetlink messages: %v", err)
	}

	return parseRTNLInterfaces(msgs)
}

// parseRTNLInterfaces unpacks rtnetlink messages and returns WireGuard
// interface names.
func parseRTNLInterfaces(msgs []syscall.NetlinkMessage) ([]string, error) {
	var ifis []string
	for _, m := range msgs {
		// Only deal with link messages, and they must have an ifinfomsg
		// structure appear before the attributes.
		if m.Header.Type != unix.RTM_NEWLINK {
			continue
		}

		if len(m.Data) < unix.SizeofIfInfomsg {
			return nil, fmt.Errorf("wglinux: rtnetlink message is too short for ifinfomsg: %d", len(m.Data))
		}

		ad, err := netlink.NewAttributeDecoder(m.Data[syscall.SizeofIfInfomsg:])
		if err != nil {
			return nil, err
		}

		// Determine the interface's name and if it's a WireGuard device.
		var (
			ifi  string
			isWG bool
		)

		for ad.Next() {
			switch ad.Type() {
			case unix.IFLA_IFNAME:
				ifi = ad.String()
			case unix.IFLA_LINKINFO:
				ad.Do(isWGKind(&isWG))
			}
		}

		if err := ad.Err(); err != nil {
			return nil, err
		}

		if isWG {
			// Found one; append it to the list.
			ifis = append(ifis, ifi)
		}
	}

	return ifis, nil
}

// wgKind is the IFLA_INFO_KIND value for WireGuard devices.
const wgKind = "wireguard"

// isWGKind parses netlink attributes to determine if a link is a WireGuard
// device, then populates ok with the result.
func isWGKind(ok *bool) func(b []byte) error {
	return func(b []byte) error {
		ad, err := netlink.NewAttributeDecoder(b)
		if err != nil {
			return err
		}

		for ad.Next() {
			if ad.Type() != unix.IFLA_INFO_KIND {
				continue
			}

			if ad.String() == wgKind {
				*ok = true
				return nil
			}
		}

		return ad.Err()
	}
}
