//go:build openbsd
// +build openbsd

package wgopenbsd

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"net"
	"os"
	"runtime"
	"time"
	"unsafe"

	"golang.org/x/sys/unix"
	"golang.zx2c4.com/wireguard/wgctrl/internal/wginternal"
	"golang.zx2c4.com/wireguard/wgctrl/internal/wgopenbsd/internal/wgh"
	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)

// ifGroupWG is the WireGuard interface group name passed to the kernel.
var ifGroupWG = [16]byte{0: 'w', 1: 'g'}

var _ wginternal.Client = &Client{}

// A Client provides access to OpenBSD WireGuard ioctl information.
type Client struct {
	// Hooks which use system calls by default, but can also be swapped out
	// during tests.
	close           func() error
	ioctlIfgroupreq func(ifg *wgh.Ifgroupreq) error
	ioctlWGDataIO   func(data *wgh.WGDataIO) error
}

// New creates a new Client and returns whether or not the ioctl interface
// is available.
func New() (*Client, bool, error) {
	// The OpenBSD ioctl interface operates on a generic AF_INET socket.
	fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
	if err != nil {
		return nil, false, err
	}

	// TODO(mdlayher): find a call to invoke here to probe for availability.
	// c.Devices won't work because it returns a "not found" error when the
	// kernel WireGuard implementation is available but the interface group
	// has no members.

	// By default, use system call implementations for all hook functions.
	return &Client{
		close:           func() error { return unix.Close(fd) },
		ioctlIfgroupreq: ioctlIfgroupreq(fd),
		ioctlWGDataIO:   ioctlWGDataIO(fd),
	}, true, nil
}

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

// Devices implements wginternal.Client.
func (c *Client) Devices() ([]*wgtypes.Device, error) {
	ifg := wgh.Ifgroupreq{
		// Query for devices in the "wg" group.
		Name: ifGroupWG,
	}

	// Determine how many device names we must allocate memory for.
	if err := c.ioctlIfgroupreq(&ifg); err != nil {
		return nil, err
	}

	// ifg.Len is size in bytes; allocate enough memory for the correct number
	// of wgh.Ifgreq and then store a pointer to the memory where the data
	// should be written (ifgrs) in ifg.Groups.
	//
	// From a thread in golang-nuts, this pattern is valid:
	// "It would be OK to pass a pointer to a struct to ioctl if the struct
	// contains a pointer to other Go memory, but the struct field must have
	// pointer type."
	// See: https://groups.google.com/forum/#!topic/golang-nuts/FfasFTZvU_o.
	ifgrs := make([]wgh.Ifgreq, ifg.Len/wgh.SizeofIfgreq)
	ifg.Groups = &ifgrs[0]

	// Now actually fetch the device names.
	if err := c.ioctlIfgroupreq(&ifg); err != nil {
		return nil, err
	}

	// Keep this alive until we're done doing the ioctl dance.
	runtime.KeepAlive(&ifg)

	devices := make([]*wgtypes.Device, 0, len(ifgrs))
	for _, ifgr := range ifgrs {
		// Remove any trailing NULL bytes from the interface names.
		d, err := c.Device(string(bytes.TrimRight(ifgr.Ifgrqu[:], "\x00")))
		if err != nil {
			return nil, err
		}

		devices = append(devices, d)
	}

	return devices, nil
}

// Device implements wginternal.Client.
func (c *Client) Device(name string) (*wgtypes.Device, error) {
	dname, err := deviceName(name)
	if err != nil {
		return nil, err
	}

	// First, specify the name of the device and determine how much memory
	// must be allocated in order to store the WGInterfaceIO structure and
	// any trailing WGPeerIO/WGAIPIOs.
	data := wgh.WGDataIO{Name: dname}

	// TODO: consider preallocating some memory to avoid a second system call
	// if it proves to be a concern.
	var mem []byte
	for {
		if err := c.ioctlWGDataIO(&data); err != nil {
			// ioctl functions always return a wrapped unix.Errno value.
			// Conform to the wgctrl contract by unwrapping some values:
			//   ENXIO: "no such device": (no such WireGuard device)
			//   ENOTTY: "inappropriate ioctl for device" (device is not a
			//	   WireGuard device)
			switch err.(*os.SyscallError).Err {
			case unix.ENXIO, unix.ENOTTY:
				return nil, os.ErrNotExist
			default:
				return nil, err
			}
		}

		if len(mem) >= int(data.Size) {
			// Allocated enough memory!
			break
		}

		// Ensure we don't unsafe cast into uninitialized memory. We need at very
		// least a single WGInterfaceIO with no peers.
		if data.Size < wgh.SizeofWGInterfaceIO {
			return nil, fmt.Errorf("wgopenbsd: kernel returned unexpected number of bytes for WGInterfaceIO: %d", data.Size)
		}

		// Allocate the appropriate amount of memory and point the kernel at
		// the first byte of our slice's backing array. When the loop continues,
		// we will check if we've allocated enough memory.
		mem = make([]byte, data.Size)
		data.Interface = (*wgh.WGInterfaceIO)(unsafe.Pointer(&mem[0]))
	}

	return parseDevice(name, data.Interface)
}

// parseDevice unpacks a Device from ifio, along with its associated peers
// and their allowed IPs.
func parseDevice(name string, ifio *wgh.WGInterfaceIO) (*wgtypes.Device, error) {
	d := &wgtypes.Device{
		Name: name,
		Type: wgtypes.OpenBSDKernel,
	}

	// The kernel populates ifio.Flags to indicate which fields are present.

	if ifio.Flags&wgh.WG_INTERFACE_HAS_PRIVATE != 0 {
		d.PrivateKey = wgtypes.Key(ifio.Private)
	}

	if ifio.Flags&wgh.WG_INTERFACE_HAS_PUBLIC != 0 {
		d.PublicKey = wgtypes.Key(ifio.Public)
	}

	if ifio.Flags&wgh.WG_INTERFACE_HAS_PORT != 0 {
		d.ListenPort = int(ifio.Port)
	}

	if ifio.Flags&wgh.WG_INTERFACE_HAS_RTABLE != 0 {
		d.FirewallMark = int(ifio.Rtable)
	}

	d.Peers = make([]wgtypes.Peer, 0, ifio.Peers_count)

	// If there were no peers, exit early so we do not advance the pointer
	// beyond the end of the WGInterfaceIO structure.
	if ifio.Peers_count == 0 {
		return d, nil
	}

	// Set our pointer to the beginning of the first peer's location in memory.
	peer := (*wgh.WGPeerIO)(unsafe.Pointer(
		uintptr(unsafe.Pointer(ifio)) + wgh.SizeofWGInterfaceIO,
	))

	for i := 0; i < int(ifio.Peers_count); i++ {
		p := parsePeer(peer)

		// Same idea, we know how many allowed IPs we need to account for, so
		// reserve the space and advance the pointer through each WGAIP structure.
		p.AllowedIPs = make([]net.IPNet, 0, peer.Aips_count)
		for j := uintptr(0); j < uintptr(peer.Aips_count); j++ {
			aip := (*wgh.WGAIPIO)(unsafe.Pointer(
				uintptr(unsafe.Pointer(peer)) + wgh.SizeofWGPeerIO + j*wgh.SizeofWGAIPIO,
			))

			p.AllowedIPs = append(p.AllowedIPs, parseAllowedIP(aip))
		}

		// Prepare for the next iteration.
		d.Peers = append(d.Peers, p)
		peer = (*wgh.WGPeerIO)(unsafe.Pointer(
			uintptr(unsafe.Pointer(peer)) + wgh.SizeofWGPeerIO +
				uintptr(peer.Aips_count)*wgh.SizeofWGAIPIO,
		))
	}

	return d, nil
}

// ConfigureDevice implements wginternal.Client.
func (c *Client) ConfigureDevice(name string, cfg wgtypes.Config) error {
	// Currently read-only: we must determine if a device belongs to this driver,
	// and if it does, return a sentinel so integration tests that configure a
	// device can be skipped.
	if _, err := c.Device(name); err != nil {
		return err
	}

	return wginternal.ErrReadOnly
}

// deviceName converts an interface name string to the format required to pass
// with wgh.WGGetServ.
func deviceName(name string) ([16]byte, error) {
	var out [unix.IFNAMSIZ]byte
	if len(name) > unix.IFNAMSIZ {
		return out, fmt.Errorf("wgopenbsd: interface name %q too long", name)
	}

	copy(out[:], name)
	return out, nil
}

// parsePeer unpacks a wgtypes.Peer from a WGPeerIO structure.
func parsePeer(pio *wgh.WGPeerIO) wgtypes.Peer {
	p := wgtypes.Peer{
		ReceiveBytes:    int64(pio.Rxbytes),
		TransmitBytes:   int64(pio.Txbytes),
		ProtocolVersion: int(pio.Protocol_version),
	}

	// Only set last handshake if a non-zero timespec was provided, matching
	// the time.Time.IsZero() behavior of internal/wglinux.
	if pio.Last_handshake.Sec > 0 && pio.Last_handshake.Nsec > 0 {
		p.LastHandshakeTime = time.Unix(
			pio.Last_handshake.Sec,
			// Conversion required for GOARCH=386.
			int64(pio.Last_handshake.Nsec),
		)
	}

	if pio.Flags&wgh.WG_PEER_HAS_PUBLIC != 0 {
		p.PublicKey = wgtypes.Key(pio.Public)
	}

	if pio.Flags&wgh.WG_PEER_HAS_PSK != 0 {
		p.PresharedKey = wgtypes.Key(pio.Psk)
	}

	if pio.Flags&wgh.WG_PEER_HAS_PKA != 0 {
		p.PersistentKeepaliveInterval = time.Duration(pio.Pka) * time.Second
	}

	if pio.Flags&wgh.WG_PEER_HAS_ENDPOINT != 0 {
		p.Endpoint = parseEndpoint(pio.Endpoint)
	}

	return p
}

// parseAllowedIP unpacks a net.IPNet from a WGAIP structure.
func parseAllowedIP(aip *wgh.WGAIPIO) net.IPNet {
	switch aip.Af {
	case unix.AF_INET:
		return net.IPNet{
			IP:   net.IP(aip.Addr[:net.IPv4len]),
			Mask: net.CIDRMask(int(aip.Cidr), 32),
		}
	case unix.AF_INET6:
		return net.IPNet{
			IP:   net.IP(aip.Addr[:]),
			Mask: net.CIDRMask(int(aip.Cidr), 128),
		}
	default:
		panicf("wgopenbsd: invalid address family for allowed IP: %+v", aip)
		return net.IPNet{}
	}
}

// parseEndpoint parses a peer endpoint from a wgh.WGIP structure.
func parseEndpoint(ep [28]byte) *net.UDPAddr {
	// sockaddr* structures have family at index 1.
	switch ep[1] {
	case unix.AF_INET:
		sa := *(*unix.RawSockaddrInet4)(unsafe.Pointer(&ep[0]))

		ep := &net.UDPAddr{
			IP:   make(net.IP, net.IPv4len),
			Port: bePort(sa.Port),
		}
		copy(ep.IP, sa.Addr[:])

		return ep
	case unix.AF_INET6:
		sa := *(*unix.RawSockaddrInet6)(unsafe.Pointer(&ep[0]))

		// TODO(mdlayher): IPv6 zone?
		ep := &net.UDPAddr{
			IP:   make(net.IP, net.IPv6len),
			Port: bePort(sa.Port),
		}
		copy(ep.IP, sa.Addr[:])

		return ep
	default:
		// No endpoint configured.
		return nil
	}
}

// bePort interprets a port integer stored in native endianness as a big
// endian value. This is necessary for proper endpoint port handling on
// little endian machines.
func bePort(port uint16) int {
	b := *(*[2]byte)(unsafe.Pointer(&port))
	return int(binary.BigEndian.Uint16(b[:]))
}

// ioctlIfgroupreq returns a function which performs the appropriate ioctl on
// fd to retrieve members of an interface group.
func ioctlIfgroupreq(fd int) func(*wgh.Ifgroupreq) error {
	return func(ifg *wgh.Ifgroupreq) error {
		return ioctl(fd, unix.SIOCGIFGMEMB, unsafe.Pointer(ifg))
	}
}

// ioctlWGDataIO returns a function which performs the appropriate ioctl on
// fd to issue a WireGuard data I/O.
func ioctlWGDataIO(fd int) func(*wgh.WGDataIO) error {
	return func(data *wgh.WGDataIO) error {
		return ioctl(fd, wgh.SIOCGWG, unsafe.Pointer(data))
	}
}

// ioctl is a raw wrapper for the ioctl system call.
func ioctl(fd int, req uint, arg unsafe.Pointer) error {
	//lint:ignore SA1019 temporarily permitted until we switch to a libc wrapper
	_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
	if errno != 0 {
		return os.NewSyscallError("ioctl", errno)
	}

	return nil
}

func panicf(format string, a ...interface{}) {
	panic(fmt.Sprintf(format, a...))
}
