// Copyright 2017 Google LLC

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

//     https://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build windows
// +build windows

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"net"
	"syscall"
	"unsafe"

	"github.com/GoogleCloudPlatform/guest-logging-go/logger"
	"golang.org/x/sys/windows"
)

var (
	ipHlpAPI = windows.NewLazySystemDLL("iphlpapi.dll")

	procAddIPAddress                    = ipHlpAPI.NewProc("AddIPAddress")
	procDeleteIPAddress                 = ipHlpAPI.NewProc("DeleteIPAddress")
	procCreateIpForwardEntry            = ipHlpAPI.NewProc("CreateIpForwardEntry")
	procDeleteIpForwardEntry            = ipHlpAPI.NewProc("DeleteIpForwardEntry")
	procGetIpForwardTable               = ipHlpAPI.NewProc("GetIpForwardTable")
	procGetIpInterfaceEntry             = ipHlpAPI.NewProc("GetIpInterfaceEntry")
	procSetIpInterfaceEntry             = ipHlpAPI.NewProc("SetIpInterfaceEntry")
	procCreateUnicastIpAddressEntry     = ipHlpAPI.NewProc("CreateUnicastIpAddressEntry")
	procInitializeUnicastIpAddressEntry = ipHlpAPI.NewProc("InitializeUnicastIpAddressEntry")
	procGetUnicastIpAddressEntry        = ipHlpAPI.NewProc("GetUnicastIpAddressEntry")
	procDeleteUnicastIpAddressEntry     = ipHlpAPI.NewProc("DeleteUnicastIpAddressEntry")
)

const (
	AF_NET   = 2
	AF_INET6 = 23
)

type MIB_IPFORWARD_TYPE DWORD

const (
	MIB_IPROUTE_TYPE_OTHER    MIB_IPFORWARD_TYPE = 1
	MIB_IPROUTE_TYPE_INVALID                     = 2
	MIB_IPROUTE_TYPE_DIRECT                      = 3
	MIB_IPROUTE_TYPE_INDIRECT                    = 4
)

type MIB_IPFORWARD_PROTO DWORD

const MIB_IPPROTO_NETMGMT MIB_IPFORWARD_PROTO = 3

type (
	in_addr struct {
		S_un struct {
			S_addr uint32
		}
	}

	SOCKADDR_IN struct {
		sin_family int16
		sin_addr   in_addr
	}

	SOCKADDR_INET struct {
		Ipv4      SOCKADDR_IN
		si_family int16
	}

	NET_LUID struct {
		Value uint64
		Info  struct {
			NetLuidIndex uint64
			IfType       uint64
		}
	}

	MIB_UNICASTIPADDRESS_ROW struct {
		Address            SOCKADDR_INET
		InterfaceLuid      NET_LUID
		InterfaceIndex     uint32
		PrefixOrigin       uint32
		SuffixOrigin       uint32
		ValidLifetime      uint32
		PreferredLifetime  uint32
		OnLinkPrefixLength uint8
		SkipAsSource       bool
	}

	IF_INDEX DWORD

	MIB_IPFORWARDROW struct {
		dwForwardDest      uint32
		dwForwardMask      uint32
		dwForwardPolicy    uint32
		dwForwardNextHop   uint32
		dwForwardIfIndex   IF_INDEX
		dwForwardType      MIB_IPFORWARD_TYPE
		dwForwardProto     MIB_IPFORWARD_PROTO
		dwForwardAge       int32
		dwForwardNextHopAS int32
		dwForwardMetric1   int32
		dwForwardMetric2   int32
		dwForwardMetric3   int32
		dwForwardMetric4   int32
		dwForwardMetric5   int32
	}
)

func addAddress(ip net.IP, mask net.IPMask, index uint32) error {
	logger.Infof("Adding %q ip address", ip.String())
	subnet, _ := mask.Size()
	// CreateUnicastIpAddressEntry only available Vista onwards.
	// AddIPAddress api supports only ipv4 address.
	if isIPv6(ip) {
		return createUnicastIpAddressEntry6(ip, uint8(subnet), index)
	}

	// https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-addipaddress
	if err := procCreateUnicastIpAddressEntry.Find(); err != nil && !isIPv6(ip) {
		return addIPAddress(ip, mask, index)
	}
	return createUnicastIpAddressEntry(ip, uint8(subnet), index)
}

func removeAddress(ip net.IP, mask net.IPMask, index uint32) error {
	logger.Infof("Removing %q ip address", ip.String())
	subnet, _ := mask.Size()
	if isIPv6(ip) {
		// Unlike ipv4 that can be added either by addIPAddress or createUnicastIpAddressEntry
		// ipv6 addresses can only be added by createUnicastIpAddressEntry6.
		// Try removing them by deleteUnicastIpAddressEntry6 only as deleteIPAddress deletes
		// IP address previously added using AddIPAddress only.
		return deleteUnicastIpAddressEntry6(ip, uint8(subnet), index)
	}

	// DeleteUnicastIpAddressEntry only available Vista onwards.
	if err := procDeleteUnicastIpAddressEntry.Find(); err != nil {
		return deleteIPAddress(ip)
	}
	return deleteUnicastIpAddressEntry(ip, index)
}

// LUID represents the locally unique identifier (LUID) for a network interface.
// https://learn.microsoft.com/en-us/windows/win32/api/ifdef/ns-ifdef-net_luid_lh
type LUID uint64

// RawSockaddrInet represents an IPv4/IPv6 address and family.
// https://learn.microsoft.com/en-us/windows/win32/api/ws2ipdef/ns-ws2ipdef-sockaddr_inet
type RawSockaddrInet struct {
	Family uint16
	data   [26]byte
}

// MIBUnicastIPAddressRow6 stores unicast IP address information used with for
// ipforwarding addresses.
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_unicastipaddress_row
type MIBUnicastIPAddressRow6 struct {
	Address            RawSockaddrInet
	InterfaceLuid      LUID
	InterfaceIndex     uint32
	PrefixOrigin       uint32
	SuffixOrigin       uint32
	ValidLifetime      uint32
	PreferredLifetime  uint32
	OnLinkPrefixLength uint8
	SkipAsSource       bool
}

// setAddr is helper function to set net.IP in a structure windows syscalls understand.
func (addr *RawSockaddrInet) setAddr(ip net.IP) {
	addr6 := (*windows.RawSockaddrInet6)(unsafe.Pointer(addr))
	addr6.Family = windows.AF_INET6
	copy(addr6.Addr[:], ip)
}

// initIpRow6 initializes the MIB_UNICASTIPADDRESS_ROW6 struct based on Ip, prefix length
// and the index.
func initIpRow6(ip net.IP, prefix uint8, index uint32) (*MIBUnicastIPAddressRow6, error) {
	ipRow := new(MIBUnicastIPAddressRow6)
	// No return value.
	procInitializeUnicastIpAddressEntry.Call(uintptr(unsafe.Pointer(ipRow)))

	ipRow.InterfaceIndex = index
	ipRow.OnLinkPrefixLength = prefix
	// https://blogs.technet.microsoft.com/rmilne/2012/02/08/fine-grained-control-when-registering-multiple-ip-addresses-on-a-network-card/
	ipRow.SkipAsSource = true

	addr := RawSockaddrInet{}
	addr.setAddr(ip)
	ipRow.Address = addr
	return ipRow, nil
}

// createUnicastIpAddressEntry6 adds a new unicast IPv6 address entry on local machine.
func createUnicastIpAddressEntry6(ip net.IP, prefix uint8, index uint32) error {
	logger.Infof("CreateUnicastIpAddressEntry6 %v, with prefix %d on interface %d", ip, prefix, index)

	ipRow, err := initIpRow6(ip, prefix, index)
	if err != nil {
		return fmt.Errorf("initialize MIB_UNICASTIPADDRESS_ROW failed: %w", err)
	}

	if ret, _, err := procCreateUnicastIpAddressEntry.Call(uintptr(unsafe.Pointer(ipRow))); ret != 0 {
		return fmt.Errorf("nonzero return code from CreateUnicastIpAddressEntry: %s, last err: %w", syscall.Errno(ret), err)
	}
	return nil
}

// deleteUnicastIpAddressEntry6 removes a unicast IPv6 address entry from local machine.
func deleteUnicastIpAddressEntry6(ip net.IP, prefix uint8, index uint32) error {
	logger.Infof("DeleteUnicastIpAddressEntry6 %v, with prefix %d on interface %d", ip, prefix, index)

	ipRow, err := initIpRow6(ip, prefix, index)
	if err != nil {
		return fmt.Errorf("initialize MIB_UNICASTIPADDRESS_ROW failed: %w", err)
	}

	if ret, _, err := procDeleteUnicastIpAddressEntry.Call(uintptr(unsafe.Pointer(ipRow))); ret != 0 {
		return fmt.Errorf("nonzero return code from DeleteUnicastIpAddressEntry: %s, last err: %w", syscall.Errno(ret), err)
	}
	return nil
}

func createUnicastIpAddressEntry(ip net.IP, prefix uint8, index uint32) error {
	ipRow := new(MIB_UNICASTIPADDRESS_ROW)
	// No return value.
	procInitializeUnicastIpAddressEntry.Call(uintptr(unsafe.Pointer(ipRow)))

	ipRow.InterfaceIndex = index
	ipRow.OnLinkPrefixLength = prefix
	// https://blogs.technet.microsoft.com/rmilne/2012/02/08/fine-grained-control-when-registering-multiple-ip-addresses-on-a-network-card/
	ipRow.SkipAsSource = true
	ipRow.Address.si_family = AF_NET
	ipRow.Address.Ipv4.sin_family = AF_NET
	ipRow.Address.Ipv4.sin_addr.S_un.S_addr = binary.LittleEndian.Uint32(ip.To4())

	if ret, _, _ := procCreateUnicastIpAddressEntry.Call(uintptr(unsafe.Pointer(ipRow))); ret != 0 {
		return fmt.Errorf("nonzero return code from CreateUnicastIpAddressEntry: %s", syscall.Errno(ret))
	}
	return nil
}

func deleteUnicastIpAddressEntry(ip net.IP, index uint32) error {
	ipRow := new(MIB_UNICASTIPADDRESS_ROW)

	ipRow.InterfaceIndex = index
	ipRow.Address.si_family = AF_NET
	ipRow.Address.Ipv4.sin_family = AF_NET
	ipRow.Address.Ipv4.sin_addr.S_un.S_addr = binary.LittleEndian.Uint32(ip.To4())

	ret, _, _ := procGetUnicastIpAddressEntry.Call(uintptr(unsafe.Pointer(ipRow)))

	// ERROR_NOT_FOUND
	if ret == 1168 {
		// This address was added by addIPAddress(), need to remove with deleteIPAddress()
		return deleteIPAddress(ip)
	}

	if ret != 0 {
		return fmt.Errorf("nonzero return code from GetUnicastIpAddressEntry: %s", syscall.Errno(ret))
	}

	if ret, _, _ := procDeleteUnicastIpAddressEntry.Call(uintptr(unsafe.Pointer(ipRow))); ret != 0 {
		return fmt.Errorf("nonzero return code from DeleteUnicastIpAddressEntry: %s", syscall.Errno(ret))
	}
	return nil
}

func addIPAddress(ip net.IP, mask net.IPMask, index uint32) error {
	var nteC int
	var nteI int

	ret, _, _ := procAddIPAddress.Call(
		uintptr(binary.LittleEndian.Uint32(ip.To4())),
		uintptr(binary.LittleEndian.Uint32(mask)),
		uintptr(index),
		uintptr(unsafe.Pointer(&nteC)),
		uintptr(unsafe.Pointer(&nteI)))
	if ret != 0 {
		return fmt.Errorf("nonzero return code from AddIPAddress: %s", syscall.Errno(ret))
	}
	return nil
}

func deleteIPAddress(ip net.IP) error {
	ip = ip.To4()
	b := make([]byte, 1)
	ai := (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0]))
	l := uint32(0)
	syscall.GetAdaptersInfo(ai, &l)

	b = make([]byte, int32(l))
	ai = (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0]))
	if err := syscall.GetAdaptersInfo(ai, &l); err != nil {
		return err
	}

	for ; ai != nil; ai = ai.Next {
		for ipl := &ai.IpAddressList; ipl != nil; ipl = ipl.Next {
			ipb := bytes.Trim(ipl.IpAddress.String[:], "\x00")
			if string(ipb) != ip.String() {
				continue
			}
			nteC := ipl.Context
			ret, _, _ := procDeleteIPAddress.Call(uintptr(nteC))
			if ret != 0 {
				return fmt.Errorf("nonzero return code from DeleteIPAddress: %s", syscall.Errno(ret))
			}
			return nil
		}
	}
	return fmt.Errorf("did not find address %s on system", ip)
}

func getIPForwardEntries() ([]ipForwardEntry, error) {
	buf := make([]byte, 1)
	size := uint32(len(buf))
	// First call gets the size of MIB_IPFORWARDTABLE.
	procGetIpForwardTable.Call(
		uintptr(unsafe.Pointer(&buf[0])),
		uintptr(unsafe.Pointer(&size)),
		0,
	)

	buf = make([]byte, size)
	if ret, _, _ := procGetIpForwardTable.Call(
		uintptr(unsafe.Pointer(&buf[0])),
		uintptr(unsafe.Pointer(&size)),
		0,
	); ret != 0 {
		return nil, fmt.Errorf("nonzero return code from GetIpForwardTable: %s", syscall.Errno(ret))
	}

	/*
	  struct MIB_IPFORWARDTABLE {
	    DWORD            dwNumEntries;
	    MIB_IPFORWARDROW table[ANY_SIZE];
	  }
	*/
	numEntries := *(*uint32)(unsafe.Pointer(&buf[0]))
	// Walk through the returned table for each entry.
	var fes []ipForwardEntry
	for i := uint32(0); i < numEntries; i++ {
		// Extract each MIB_IPFORWARDROW from MIB_IPFORWARDTABLE
		fr := *((*MIB_IPFORWARDROW)(unsafe.Pointer(
			(uintptr(unsafe.Pointer(&buf[0])) + unsafe.Sizeof(numEntries)) + (unsafe.Sizeof(MIB_IPFORWARDROW{}) * uintptr(i)),
		)))
		fd := make([]byte, 4)
		binary.LittleEndian.PutUint32(fd, uint32(fr.dwForwardDest))
		fm := make([]byte, 4)
		binary.LittleEndian.PutUint32(fm, uint32(fr.dwForwardMask))
		nh := make([]byte, 4)
		binary.LittleEndian.PutUint32(nh, uint32(fr.dwForwardNextHop))
		fe := ipForwardEntry{
			ipForwardDest:    net.IP(fd),
			ipForwardMask:    net.IPMask(fm),
			ipForwardNextHop: net.IP(nh),
			ipForwardIfIndex: int32(fr.dwForwardIfIndex),
			ipForwardMetric1: fr.dwForwardMetric1,
		}
		fes = append(fes, fe)
	}

	return fes, nil
}

func addIPForwardEntry(fe ipForwardEntry) error {
	// https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-createipforwardentry

	fr := &MIB_IPFORWARDROW{
		dwForwardDest:      binary.LittleEndian.Uint32(fe.ipForwardDest.To4()),
		dwForwardMask:      binary.LittleEndian.Uint32(fe.ipForwardMask),
		dwForwardPolicy:    0, // unused
		dwForwardNextHop:   binary.LittleEndian.Uint32(fe.ipForwardNextHop.To4()),
		dwForwardIfIndex:   IF_INDEX(fe.ipForwardIfIndex),
		dwForwardType:      MIB_IPROUTE_TYPE_INDIRECT, // unused
		dwForwardProto:     MIB_IPPROTO_NETMGMT,
		dwForwardAge:       0, // unused
		dwForwardNextHopAS: 0, // unused
		dwForwardMetric1:   fe.ipForwardMetric1,
		dwForwardMetric2:   -1, // unused
		dwForwardMetric3:   -1, // unused
		dwForwardMetric4:   -1, // unused
		dwForwardMetric5:   -1, // unused
	}

	if ret, _, _ := procCreateIpForwardEntry.Call(uintptr(unsafe.Pointer(fr))); ret != 0 {
		return fmt.Errorf("nonzero return code from CreateIpForwardEntry: %s", syscall.Errno(ret))
	}
	return nil
}
