//go:build windows && cgo

package ingress

/*
#include <iphlpapi.h>
#include <icmpapi.h>
*/
import "C"
import (
	"context"
	"encoding/binary"
	"fmt"
	"net/netip"
	"runtime/debug"
	"sync"
	"syscall"
	"time"
	"unsafe"

	"github.com/google/gopacket/layers"
	"github.com/pkg/errors"
	"github.com/rs/zerolog"
	"golang.org/x/net/icmp"
	"golang.org/x/net/ipv4"
	"golang.org/x/net/ipv6"

	"github.com/cloudflare/cloudflared/packet"
)

const (
	// Value defined in https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw
	AF_INET6          = 23
	icmpEchoReplyCode = 0
	nullParameter     = uintptr(0)
)

var (
	Iphlpapi             = syscall.NewLazyDLL("Iphlpapi.dll")
	IcmpCreateFile_proc  = Iphlpapi.NewProc("IcmpCreateFile")
	Icmp6CreateFile_proc = Iphlpapi.NewProc("Icmp6CreateFile")
	IcmpSendEcho_proc    = Iphlpapi.NewProc("IcmpSendEcho")
	Icmp6SendEcho_proc   = Iphlpapi.NewProc("Icmp6SendEcho2")
	echoReplySize        = unsafe.Sizeof(echoReply{})
	echoV6ReplySize      = unsafe.Sizeof(echoV6Reply{})
	icmpv6ErrMessageSize = 8
	ioStatusBlockSize    = unsafe.Sizeof(ioStatusBlock{})
	endian               = binary.LittleEndian
)

// IP_STATUS code, see https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmp_echo_reply32#members
// for possible values
type ipStatus uint32

const (
	success     ipStatus = 0
	bufTooSmall          = iota + 11000
	destNetUnreachable
	destHostUnreachable
	destProtocolUnreachable
	destPortUnreachable
	noResources
	badOption
	hwError
	packetTooBig
	reqTimedOut
	badReq
	badRoute
	ttlExpiredTransit
	ttlExpiredReassembly
	paramProblem
	sourceQuench
	optionTooBig
	badDestination
	// Can be returned for malformed ICMP packets
	generalFailure = 11050
)

// Additional IP_STATUS codes for ICMPv6  https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmpv6_echo_reply_lh#members
const (
	ipv6DestUnreachable ipStatus = iota + 11040
	ipv6TimeExceeded
	ipv6BadHeader
	ipv6UnrecognizedNextHeader
	ipv6ICMPError
	ipv6DestScopeMismatch
)

func (is ipStatus) String() string {
	switch is {
	case success:
		return "Success"
	case bufTooSmall:
		return "The reply buffer too small"
	case destNetUnreachable:
		return "The destination network was unreachable"
	case destHostUnreachable:
		return "The destination host was unreachable"
	case destProtocolUnreachable:
		return "The destination protocol was unreachable"
	case destPortUnreachable:
		return "The destination port was unreachable"
	case noResources:
		return "Insufficient IP resources were available"
	case badOption:
		return "A bad IP option was specified"
	case hwError:
		return "A hardware error occurred"
	case packetTooBig:
		return "The packet was too big"
	case reqTimedOut:
		return "The request timed out"
	case badReq:
		return "Bad request"
	case badRoute:
		return "Bad route"
	case ttlExpiredTransit:
		return "The TTL expired in transit"
	case ttlExpiredReassembly:
		return "The TTL expired during fragment reassembly"
	case paramProblem:
		return "A parameter problem"
	case sourceQuench:
		return "Datagrams are arriving too fast to be processed and datagrams may have been discarded"
	case optionTooBig:
		return "The IP option was too big"
	case badDestination:
		return "Bad destination"
	case ipv6DestUnreachable:
		return "IPv6 destination unreachable"
	case ipv6TimeExceeded:
		return "IPv6 time exceeded"
	case ipv6BadHeader:
		return "IPv6 bad IP header"
	case ipv6UnrecognizedNextHeader:
		return "IPv6 unrecognized next header"
	case ipv6ICMPError:
		return "IPv6 ICMP error"
	case ipv6DestScopeMismatch:
		return "IPv6 destination scope ID mismatch"
	case generalFailure:
		return "The ICMP packet might be malformed"
	default:
		return fmt.Sprintf("Unknown ip status %d", is)
	}
}

// https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-ip_option_information
type ipOption struct {
	TTL         uint8
	Tos         uint8
	Flags       uint8
	OptionsSize uint8
	OptionsData uintptr
}

// https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmp_echo_reply
type echoReply struct {
	Address       uint32
	Status        ipStatus
	RoundTripTime uint32
	DataSize      uint16
	Reserved      uint16
	// The pointer size defers between 32-bit and 64-bit platforms
	DataPointer *byte
	Options     ipOption
}

type echoV6Reply struct {
	Address       ipv6AddrEx
	Status        ipStatus
	RoundTripTime uint32
}

// https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-ipv6_address_ex
// All the fields are in network byte order. The memory alignment is 4 bytes
type ipv6AddrEx struct {
	port uint16
	// flowInfo is uint32. Because of field alignment, when we cast reply buffer to ipv6AddrEx, it starts at the 5th byte
	// But looking at the raw bytes, flowInfo starts at the 3rd byte. We device flowInfo into 2 uint16 so it's aligned
	flowInfoUpper uint16
	flowInfoLower uint16
	addr          [8]uint16
	scopeID       uint32
}

// https://docs.microsoft.com/en-us/windows/win32/winsock/sockaddr-2
type sockAddrIn6 struct {
	family int16
	// Can't embed ipv6AddrEx, that changes the memory alignment
	port     uint16
	flowInfo uint32
	addr     [16]byte
	scopeID  uint32
}

func newSockAddrIn6(addr netip.Addr) (*sockAddrIn6, error) {
	if !addr.Is6() {
		return nil, fmt.Errorf("%s is not IPv6", addr)
	}
	return &sockAddrIn6{
		family: AF_INET6,
		port:   10,
		addr:   addr.As16(),
	}, nil
}

// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_status_block#syntax
type ioStatusBlock struct {
	// The first field is an union of NTSTATUS and PVOID. NTSTATUS is int32 while PVOID depends on the platform.
	// We model it as uintptr whose size depends on if the platform is 32-bit or 64-bit
	// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55
	statusOrPointer uintptr
	information     uintptr
}

type icmpProxy struct {
	// An open handle that can send ICMP requests https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpcreatefile
	handle uintptr
	// This is a ICMPv6 if srcSocketAddr is not nil
	srcSocketAddr *sockAddrIn6
	logger        *zerolog.Logger
	// A pool of reusable *packet.Encoder
	encoderPool sync.Pool
}

func newICMPProxy(listenIP netip.Addr, zone string, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) {
	var (
		srcSocketAddr *sockAddrIn6
		handle        uintptr
		err           error
	)
	if listenIP.Is4() {
		handle, _, err = IcmpCreateFile_proc.Call()
	} else {
		srcSocketAddr, err = newSockAddrIn6(listenIP)
		if err != nil {
			return nil, err
		}
		handle, _, err = Icmp6CreateFile_proc.Call()
	}
	// Windows procedure calls always return non-nil error constructed from the result of GetLastError.
	// Caller need to inspect the primary returned value
	if syscall.Handle(handle) == syscall.InvalidHandle {
		return nil, errors.Wrap(err, "invalid ICMP handle")
	}
	return &icmpProxy{
		handle:        handle,
		srcSocketAddr: srcSocketAddr,
		logger:        logger,
		encoderPool: sync.Pool{
			New: func() any {
				return packet.NewEncoder()
			},
		},
	}, nil
}

func (ip *icmpProxy) Serve(ctx context.Context) error {
	<-ctx.Done()
	syscall.CloseHandle(syscall.Handle(ip.handle))
	return ctx.Err()
}

// Request sends an ICMP echo request and wait for a reply or timeout.
// The async version of Win32 APIs take a callback whose memory is not garbage collected, so we use the synchronous version.
// It's possible that a slow request will block other requests, so we set the timeout to only 1s.
func (ip *icmpProxy) Request(pk *packet.ICMP, responder packet.FunnelUniPipe) error {
	if pk == nil {
		return errPacketNil
	}
	defer func() {
		if r := recover(); r != nil {
			ip.logger.Error().Interface("error", r).Msgf("Recover panic from sending icmp request/response, error %s", debug.Stack())
		}
	}()

	echo, err := getICMPEcho(pk.Message)
	if err != nil {
		return err
	}
	respData, err := ip.icmpEchoRoundtrip(pk.Dst, echo)
	if err != nil {
		ip.logger.Err(err).Msg("ICMP echo roundtrip failed")
		return err
	}

	err = ip.handleEchoReply(pk, echo, respData, responder)
	if err != nil {
		return errors.Wrap(err, "failed to handle ICMP echo reply")
	}
	return nil
}

func (ip *icmpProxy) handleEchoReply(request *packet.ICMP, echoReq *icmp.Echo, data []byte, responder packet.FunnelUniPipe) error {
	var replyType icmp.Type
	if request.Dst.Is4() {
		replyType = ipv4.ICMPTypeEchoReply
	} else {
		replyType = ipv6.ICMPTypeEchoReply
	}

	pk := packet.ICMP{
		IP: &packet.IP{
			Src:      request.Dst,
			Dst:      request.Src,
			Protocol: layers.IPProtocol(request.Type.Protocol()),
			TTL:      packet.DefaultTTL,
		},
		Message: &icmp.Message{
			Type: replyType,
			Code: icmpEchoReplyCode,
			Body: &icmp.Echo{
				ID:   echoReq.ID,
				Seq:  echoReq.Seq,
				Data: data,
			},
		},
	}

	cachedEncoder := ip.encoderPool.Get()
	// The encoded packet is a slice to of the encoder, so we shouldn't return the encoder back to the pool until
	// the encoded packet is sent.
	defer ip.encoderPool.Put(cachedEncoder)
	encoder, ok := cachedEncoder.(*packet.Encoder)
	if !ok {
		return fmt.Errorf("encoderPool returned %T, expect *packet.Encoder", cachedEncoder)
	}

	serializedPacket, err := encoder.Encode(&pk)
	if err != nil {
		return err
	}
	return responder.SendPacket(request.Src, serializedPacket)
}

func (ip *icmpProxy) icmpEchoRoundtrip(dst netip.Addr, echo *icmp.Echo) ([]byte, error) {
	if dst.Is6() {
		if ip.srcSocketAddr == nil {
			return nil, fmt.Errorf("cannot send ICMPv6 using ICMPv4 proxy")
		}
		resp, err := ip.icmp6SendEcho(dst, echo)
		if err != nil {
			return nil, errors.Wrap(err, "failed to send/receive ICMPv6 echo")
		}
		return resp.data, nil
	}
	if ip.srcSocketAddr != nil {
		return nil, fmt.Errorf("cannot send ICMPv4 using ICMPv6 proxy")
	}
	resp, err := ip.icmpSendEcho(dst, echo)
	if err != nil {
		return nil, errors.Wrap(err, "failed to send/receive ICMPv4 echo")
	}
	return resp.data, nil
}

/*
	Wrapper to call https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpsendecho
	Parameters:
	- IcmpHandle: Handle created by IcmpCreateFile
	- DestinationAddress: IPv4 in the form of https://docs.microsoft.com/en-us/windows/win32/api/inaddr/ns-inaddr-in_addr#syntax
	- RequestData: A pointer to echo data
	- RequestSize: Number of bytes in buffer pointed by echo data
	- RequestOptions: IP header options
	- ReplyBuffer: A pointer to the buffer for echoReply, options and data
	- ReplySize: Number of bytes allocated for ReplyBuffer
	- Timeout: Timeout in milliseconds to wait for a reply
	Returns:
	- the number of replies in uint32 https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpsendecho#return-value
	To retain the reference allocated objects, conversion from pointer to uintptr must happen as arguments to the
	syscall function
*/
func (ip *icmpProxy) icmpSendEcho(dst netip.Addr, echo *icmp.Echo) (*echoResp, error) {
	dataSize := len(echo.Data)
	replySize := echoReplySize + uintptr(dataSize)
	replyBuf := make([]byte, replySize)
	noIPHeaderOption := nullParameter
	inAddr, err := inAddrV4(dst)
	if err != nil {
		return nil, err
	}
	replyCount, _, err := IcmpSendEcho_proc.Call(
		ip.handle,
		uintptr(inAddr),
		uintptr(unsafe.Pointer(&echo.Data[0])),
		uintptr(dataSize),
		noIPHeaderOption,
		uintptr(unsafe.Pointer(&replyBuf[0])),
		replySize,
		icmpRequestTimeoutMs,
	)
	if replyCount == 0 {
		// status is returned in 5th to 8th byte of reply buffer
		if status, parseErr := unmarshalIPStatus(replyBuf[4:8]); parseErr == nil && status != success {
			return nil, errors.Wrapf(err, "received ip status: %s", status)
		}
		return nil, errors.Wrap(err, "did not receive ICMP echo reply")
	} else if replyCount > 1 {
		ip.logger.Warn().Msgf("Received %d ICMP echo replies, only sending 1 back", replyCount)
	}
	return newEchoResp(replyBuf)
}

// Third definition of https://docs.microsoft.com/en-us/windows/win32/api/inaddr/ns-inaddr-in_addr#syntax is address in uint32
func inAddrV4(ip netip.Addr) (uint32, error) {
	if !ip.Is4() {
		return 0, fmt.Errorf("%s is not IPv4", ip)
	}
	v4 := ip.As4()
	return endian.Uint32(v4[:]), nil
}

type echoResp struct {
	reply *echoReply
	data  []byte
}

func newEchoResp(replyBuf []byte) (*echoResp, error) {
	if len(replyBuf) == 0 {
		return nil, fmt.Errorf("reply buffer is empty")
	}
	// This is pattern 1 of https://pkg.go.dev/unsafe@master#Pointer, conversion of *replyBuf to *echoReply
	// replyBuf size is larger than echoReply
	reply := *(*echoReply)(unsafe.Pointer(&replyBuf[0]))
	if reply.Status != success {
		return nil, fmt.Errorf("status %d", reply.Status)
	}
	dataBufStart := len(replyBuf) - int(reply.DataSize)
	if dataBufStart < int(echoReplySize) {
		return nil, fmt.Errorf("reply buffer size %d is too small to hold data of size %d", len(replyBuf), int(reply.DataSize))
	}
	return &echoResp{
		reply: &reply,
		data:  replyBuf[dataBufStart:],
	}, nil
}

/*
	Wrapper to call https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmp6sendecho2
	Parameters:
	- IcmpHandle: Handle created by Icmp6CreateFile
	- Event (optional): Event object to be signaled when a reply arrives
	- ApcRoutine (optional): Routine to call when the calling thread is in an alertable thread and a reply arrives
	- ApcContext (optional): Optional parameter to ApcRoutine
	- SourceAddress: Source address of the request
	- DestinationAddress: Destination address of the request
	- RequestData: A pointer to echo data
	- RequestSize: Number of bytes in buffer pointed by echo data
	- RequestOptions (optional): A pointer to the IPv6 header options
	- ReplyBuffer: A pointer to the buffer for echoReply, options and data
	- ReplySize: Number of bytes allocated for ReplyBuffer
	- Timeout: Timeout in milliseconds to wait for a reply
	Returns:
	- the number of replies in uint32
	To retain the reference allocated objects, conversion from pointer to uintptr must happen as arguments to the
	syscall function
*/

func (ip *icmpProxy) icmp6SendEcho(dst netip.Addr, echo *icmp.Echo) (*echoV6Resp, error) {
	dstAddr, err := newSockAddrIn6(dst)
	if err != nil {
		return nil, err
	}
	dataSize := len(echo.Data)
	// Reply buffer needs to be big enough to hold an echoV6Reply, echo data, 8 bytes for ICMP error message
	// and ioStatusBlock
	replySize := echoV6ReplySize + uintptr(dataSize) + uintptr(icmpv6ErrMessageSize) + ioStatusBlockSize
	replyBuf := make([]byte, replySize)
	noEvent := nullParameter
	noApcRoutine := nullParameter
	noAppCtx := nullParameter
	noIPHeaderOption := nullParameter
	replyCount, _, err := Icmp6SendEcho_proc.Call(
		ip.handle,
		noEvent,
		noApcRoutine,
		noAppCtx,
		uintptr(unsafe.Pointer(ip.srcSocketAddr)),
		uintptr(unsafe.Pointer(dstAddr)),
		uintptr(unsafe.Pointer(&echo.Data[0])),
		uintptr(dataSize),
		noIPHeaderOption,
		uintptr(unsafe.Pointer(&replyBuf[0])),
		replySize,
		icmpRequestTimeoutMs,
	)
	if replyCount == 0 {
		// status is in the 4 bytes after ipv6AddrEx. The reply buffer size is at least size of ipv6AddrEx + 4
		if status, parseErr := unmarshalIPStatus(replyBuf[unsafe.Sizeof(ipv6AddrEx{}) : unsafe.Sizeof(ipv6AddrEx{})+4]); parseErr == nil && status != success {
			return nil, fmt.Errorf("received ip status: %s", status)
		}
		return nil, errors.Wrap(err, "did not receive ICMP echo reply")
	} else if replyCount > 1 {
		ip.logger.Warn().Msgf("Received %d ICMP echo replies, only sending 1 back", replyCount)
	}
	return newEchoV6Resp(replyBuf, dataSize)
}

type echoV6Resp struct {
	reply *echoV6Reply
	data  []byte
}

func newEchoV6Resp(replyBuf []byte, dataSize int) (*echoV6Resp, error) {
	if len(replyBuf) == 0 {
		return nil, fmt.Errorf("reply buffer is empty")
	}
	reply := *(*echoV6Reply)(unsafe.Pointer(&replyBuf[0]))
	if reply.Status != success {
		return nil, fmt.Errorf("status %d", reply.Status)
	}
	if uintptr(len(replyBuf)) < unsafe.Sizeof(reply)+uintptr(dataSize) {
		return nil, fmt.Errorf("reply buffer size %d is too small to hold reply size %d + data size %d", len(replyBuf), echoV6ReplySize, dataSize)
	}
	return &echoV6Resp{
		reply: &reply,
		data:  replyBuf[echoV6ReplySize : echoV6ReplySize+uintptr(dataSize)],
	}, nil
}

func unmarshalIPStatus(replyBuf []byte) (ipStatus, error) {
	if len(replyBuf) != 4 {
		return 0, fmt.Errorf("ipStatus needs to be 4 bytes, got %d", len(replyBuf))
	}
	return ipStatus(endian.Uint32(replyBuf)), nil
}
