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

package natlab

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

	"tailscale.com/util/mak"
)

// FirewallType is the type of filtering a stateful firewall
// does. Values express different modes defined by RFC 4787.
type FirewallType int

const (
	// AddressAndPortDependentFirewall specifies a destination
	// address-and-port dependent firewall. Outbound traffic to an
	// ip:port authorizes traffic from that ip:port exactly, and
	// nothing else.
	AddressAndPortDependentFirewall FirewallType = iota
	// AddressDependentFirewall specifies a destination address
	// dependent firewall. Once outbound traffic has been seen to an
	// IP address, that IP address can talk back from any port.
	AddressDependentFirewall
	// EndpointIndependentFirewall specifies a destination endpoint
	// independent firewall. Once outbound traffic has been seen from
	// a source, anyone can talk back to that source.
	EndpointIndependentFirewall
)

// fwKey is the lookup key for a firewall session. While it contains a
// 4-tuple ({src,dst} {ip,port}), some FirewallTypes will zero out
// some fields, so in practice the key is either a 2-tuple (src only),
// 3-tuple (src ip+port and dst ip) or 4-tuple (src+dst ip+port).
type fwKey struct {
	src netip.AddrPort
	dst netip.AddrPort
}

// key returns an fwKey for the given src and dst, trimmed according
// to the FirewallType. fwKeys are always constructed from the
// "outbound" point of view (i.e. src is the "trusted" side of the
// world), it's the caller's responsibility to swap src and dst in the
// call to key when processing packets inbound from the "untrusted"
// world.
func (s FirewallType) key(src, dst netip.AddrPort) fwKey {
	k := fwKey{src: src}
	switch s {
	case EndpointIndependentFirewall:
	case AddressDependentFirewall:
		k.dst = netip.AddrPortFrom(dst.Addr(), k.dst.Port())
	case AddressAndPortDependentFirewall:
		k.dst = dst
	default:
		panic(fmt.Sprintf("unknown firewall selectivity %v", s))
	}
	return k
}

// DefaultSessionTimeout is the default timeout for a firewall
// session.
const DefaultSessionTimeout = 30 * time.Second

// Firewall is a simple stateful firewall that allows all outbound
// traffic and filters inbound traffic based on recently seen outbound
// traffic. Its HandlePacket method should be attached to a Machine to
// give it a stateful firewall.
type Firewall struct {
	// SessionTimeout is the lifetime of idle sessions in the firewall
	// state. Packets transiting from the TrustedInterface reset the
	// session lifetime to SessionTimeout. If zero,
	// DefaultSessionTimeout is used.
	SessionTimeout time.Duration
	// Type specifies how precisely return traffic must match
	// previously seen outbound traffic to be allowed. Defaults to
	// AddressAndPortDependentFirewall.
	Type FirewallType
	// TrustedInterface is an optional interface that is considered
	// trusted in addition to PacketConns local to the Machine. All
	// other interfaces can only respond to traffic from
	// TrustedInterface or the local host.
	TrustedInterface *Interface
	// TimeNow is a function returning the current time. If nil,
	// time.Now is used.
	TimeNow func() time.Time

	// TODO: refresh directionality: outbound-only, both

	mu   sync.Mutex
	seen map[fwKey]time.Time // session -> deadline
}

func (f *Firewall) timeNow() time.Time {
	if f.TimeNow != nil {
		return f.TimeNow()
	}
	return time.Now()
}

// Reset drops all firewall state, forgetting all flows.
func (f *Firewall) Reset() {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.seen = nil
}

func (f *Firewall) HandleOut(p *Packet, oif *Interface) *Packet {
	f.mu.Lock()
	defer f.mu.Unlock()

	k := f.Type.key(p.Src, p.Dst)
	mak.Set(&f.seen, k, f.timeNow().Add(f.sessionTimeoutLocked()))
	p.Trace("firewall out ok")
	return p
}

func (f *Firewall) HandleIn(p *Packet, iif *Interface) *Packet {
	f.mu.Lock()
	defer f.mu.Unlock()

	// reverse src and dst because the session table is from the POV
	// of outbound packets.
	k := f.Type.key(p.Dst, p.Src)
	now := f.timeNow()
	if now.After(f.seen[k]) {
		p.Trace("firewall drop")
		return nil
	}
	p.Trace("firewall in ok")
	return p
}

func (f *Firewall) HandleForward(p *Packet, iif *Interface, oif *Interface) *Packet {
	if iif == f.TrustedInterface {
		// Treat just like a locally originated packet
		return f.HandleOut(p, oif)
	}
	if oif != f.TrustedInterface {
		// Not a possible return packet from our trusted interface, drop.
		p.Trace("firewall drop, unexpected oif")
		return nil
	}
	// Otherwise, a session must exist, same as HandleIn.
	return f.HandleIn(p, iif)
}

func (f *Firewall) sessionTimeoutLocked() time.Duration {
	if f.SessionTimeout == 0 {
		return DefaultSessionTimeout
	}
	return f.SessionTimeout
}
