// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package vnet

import (
	"log"
	"math/rand/v2"
	"net/netip"
	"time"

	"tailscale.com/util/mak"
)

// easyAFNAT is an "Endpoint Independent" NAT, like Linux and most home routers
// (many of which are Linux), but with only address filtering, not address+port
// filtering.
//
// James says these are used by "anyone with “voip helpers” turned on"
// "which is a lot of home modem routers" ... "probably like most of the zyxel
// type things".
type easyAFNAT struct {
	pool    IPPool
	wanIP   netip.Addr
	out     map[netip.Addr]portMappingAndTime
	in      map[uint16]lanAddrAndTime
	lastOut map[srcAPDstAddrTuple]time.Time // (lan:port, wan:port) => last packet out time
}

type srcAPDstAddrTuple struct {
	src netip.AddrPort
	dst netip.Addr
}

func init() {
	registerNATType(EasyAFNAT, func(p IPPool) (NATTable, error) {
		return &easyAFNAT{pool: p, wanIP: p.WANIP()}, nil
	})
}

func (n *easyAFNAT) IsPublicPortUsed(ap netip.AddrPort) bool {
	if ap.Addr() != n.wanIP {
		return false
	}
	_, ok := n.in[ap.Port()]
	return ok
}

func (n *easyAFNAT) PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wanSrc netip.AddrPort) {
	mak.Set(&n.lastOut, srcAPDstAddrTuple{src, dst.Addr()}, at)
	if pm, ok := n.out[src.Addr()]; ok {
		// Existing flow.
		// TODO: bump timestamp
		return netip.AddrPortFrom(n.wanIP, pm.port)
	}

	// Loop through all 32k high (ephemeral) ports, starting at a random
	// position and looping back around to the start.
	start := rand.N(uint16(32 << 10))
	for off := range uint16(32 << 10) {
		port := 32<<10 + (start+off)%(32<<10)
		if _, ok := n.in[port]; !ok {
			wanAddr := netip.AddrPortFrom(n.wanIP, port)
			if n.pool.IsPublicPortUsed(wanAddr) {
				continue
			}

			// Found a free port.
			mak.Set(&n.out, src.Addr(), portMappingAndTime{port: port, at: at})
			mak.Set(&n.in, port, lanAddrAndTime{lanAddr: src, at: at})
			return wanAddr
		}
	}
	return netip.AddrPort{} // failed to allocate a mapping; TODO: fire an alert?
}

func (n *easyAFNAT) PickIncomingDst(src, dst netip.AddrPort, at time.Time) (lanDst netip.AddrPort) {
	if dst.Addr() != n.wanIP {
		return netip.AddrPort{} // drop; not for us. shouldn't happen if natlabd routing isn't broken.
	}
	lanDst = n.in[dst.Port()].lanAddr

	// Stateful firewall: drop incoming packets that don't have traffic out.
	// TODO(bradfitz): verify Linux does this in the router code, not in the NAT code.
	if t, ok := n.lastOut[srcAPDstAddrTuple{lanDst, src.Addr()}]; !ok || at.Sub(t) > 300*time.Second {
		log.Printf("Drop incoming packet from %v to %v; no recent outgoing packet", src, dst)
		return netip.AddrPort{}
	}

	return lanDst
}
