//go:build darwin || linux

package ingress

// This file extracts logic shared by Linux and Darwin implementation if ICMPProxy.

import (
	"fmt"
	"net"
	"net/netip"
	"sync/atomic"

	"github.com/google/gopacket/layers"
	"github.com/rs/zerolog"
	"golang.org/x/net/icmp"

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

// Opens a non-privileged ICMP socket on Linux and Darwin
func newICMPConn(listenIP netip.Addr) (*icmp.PacketConn, error) {
	if listenIP.Is4() {
		return icmp.ListenPacket("udp4", listenIP.String())
	}
	return icmp.ListenPacket("udp6", listenIP.String())
}

func netipAddr(addr net.Addr) (netip.Addr, bool) {
	udpAddr, ok := addr.(*net.UDPAddr)
	if !ok {
		return netip.Addr{}, false
	}

	return udpAddr.AddrPort().Addr(), true
}

type flow3Tuple struct {
	srcIP          netip.Addr
	dstIP          netip.Addr
	originalEchoID int
}

// icmpEchoFlow implements the packet.Funnel interface.
type icmpEchoFlow struct {
	*packet.ActivityTracker
	closeCallback  func() error
	closed         *atomic.Bool
	src            netip.Addr
	originConn     *icmp.PacketConn
	responder      ICMPResponder
	assignedEchoID int
	originalEchoID int
}

func newICMPEchoFlow(src netip.Addr, closeCallback func() error, originConn *icmp.PacketConn, responder ICMPResponder, assignedEchoID, originalEchoID int) *icmpEchoFlow {
	return &icmpEchoFlow{
		ActivityTracker: packet.NewActivityTracker(),
		closeCallback:   closeCallback,
		closed:          &atomic.Bool{},
		src:             src,
		originConn:      originConn,
		responder:       responder,
		assignedEchoID:  assignedEchoID,
		originalEchoID:  originalEchoID,
	}
}

func (ief *icmpEchoFlow) Equal(other packet.Funnel) bool {
	otherICMPFlow, ok := other.(*icmpEchoFlow)
	if !ok {
		return false
	}
	if otherICMPFlow.src != ief.src {
		return false
	}
	if otherICMPFlow.originalEchoID != ief.originalEchoID {
		return false
	}
	if otherICMPFlow.assignedEchoID != ief.assignedEchoID {
		return false
	}
	return true
}

func (ief *icmpEchoFlow) Close() error {
	ief.closed.Store(true)
	return ief.closeCallback()
}

func (ief *icmpEchoFlow) IsClosed() bool {
	return ief.closed.Load()
}

// sendToDst rewrites the echo ID to the one assigned to this flow
func (ief *icmpEchoFlow) sendToDst(dst netip.Addr, msg *icmp.Message) error {
	ief.UpdateLastActive()
	originalEcho, err := getICMPEcho(msg)
	if err != nil {
		return err
	}
	sendMsg := icmp.Message{
		Type: msg.Type,
		Code: msg.Code,
		Body: &icmp.Echo{
			ID:   ief.assignedEchoID,
			Seq:  originalEcho.Seq,
			Data: originalEcho.Data,
		},
	}
	// For IPv4, the pseudoHeader is not used because the checksum is always calculated
	var pseudoHeader []byte = nil
	serializedPacket, err := sendMsg.Marshal(pseudoHeader)
	if err != nil {
		return err
	}
	_, err = ief.originConn.WriteTo(serializedPacket, &net.UDPAddr{
		IP: dst.AsSlice(),
	})
	return err
}

// returnToSrc rewrites the echo ID to the original echo ID from the eyeball
func (ief *icmpEchoFlow) returnToSrc(reply *echoReply) error {
	ief.UpdateLastActive()
	reply.echo.ID = ief.originalEchoID
	reply.msg.Body = reply.echo
	pk := packet.ICMP{
		IP: &packet.IP{
			Src:      reply.from,
			Dst:      ief.src,
			Protocol: layers.IPProtocol(reply.msg.Type.Protocol()),
			TTL:      packet.DefaultTTL,
		},
		Message: reply.msg,
	}
	return ief.responder.ReturnPacket(&pk)
}

type echoReply struct {
	from netip.Addr
	msg  *icmp.Message
	echo *icmp.Echo
}

func parseReply(from net.Addr, rawMsg []byte) (*echoReply, error) {
	fromAddr, ok := netipAddr(from)
	if !ok {
		return nil, fmt.Errorf("cannot convert %s to netip.Addr", from)
	}
	proto := layers.IPProtocolICMPv4
	if fromAddr.Is6() {
		proto = layers.IPProtocolICMPv6
	}
	msg, err := icmp.ParseMessage(int(proto), rawMsg)
	if err != nil {
		return nil, err
	}
	echo, err := getICMPEcho(msg)
	if err != nil {
		return nil, err
	}
	return &echoReply{
		from: fromAddr,
		msg:  msg,
		echo: echo,
	}, nil
}

func toICMPEchoFlow(funnel packet.Funnel) (*icmpEchoFlow, error) {
	icmpFlow, ok := funnel.(*icmpEchoFlow)
	if !ok {
		return nil, fmt.Errorf("%v is not *ICMPEchoFunnel", funnel)
	}
	return icmpFlow, nil
}

func createShouldReplaceFunnelFunc(logger *zerolog.Logger, responder ICMPResponder, pk *packet.ICMP, originalEchoID int) func(packet.Funnel) bool {
	return func(existing packet.Funnel) bool {
		existingFlow, err := toICMPEchoFlow(existing)
		if err != nil {
			logger.Err(err).
				Str("src", pk.Src.String()).
				Str("dst", pk.Dst.String()).
				Int("originalEchoID", originalEchoID).
				Msg("Funnel of wrong type found")
			return true
		}
		// Each quic connection should have a unique muxer.
		// If the existing flow has a different muxer, there's a new quic connection where return packets should be
		// routed. Otherwise, return packets will be send to the first observed incoming connection, rather than the
		// most recently observed connection.
		if existingFlow.responder.ConnectionIndex() != responder.ConnectionIndex() {
			logger.Debug().
				Str("src", pk.Src.String()).
				Str("dst", pk.Dst.String()).
				Int("originalEchoID", originalEchoID).
				Msg("Replacing funnel with new responder")
			return true
		}
		return false
	}
}
