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

//go:build darwin || freebsd

package routetable

import (
	"bufio"
	"fmt"
	"net"
	"net/netip"
	"runtime"
	"sort"
	"strings"
	"syscall"

	"golang.org/x/net/route"
	"golang.org/x/sys/unix"
	"tailscale.com/net/netmon"
	"tailscale.com/types/logger"
)

type RouteEntryBSD struct {
	// GatewayInterface is the name of the interface specified as a gateway
	// for this route, if any.
	GatewayInterface string
	// GatewayIdx is the index of the interface specified as a gateway for
	// this route, if any.
	GatewayIdx int
	// GatewayAddr is the link-layer address of the gateway for this route,
	// if any.
	GatewayAddr string
	// Flags contains a string representation of common flags for this
	// route.
	Flags []string
	// RawFlags contains the raw flags that were returned by the operating
	// system for this route.
	RawFlags int
}

// Format implements the fmt.Formatter interface.
func (r RouteEntryBSD) Format(f fmt.State, verb rune) {
	logger.ArgWriter(func(w *bufio.Writer) {
		var pstart bool
		pr := func(format string, args ...any) {
			if pstart {
				fmt.Fprintf(w, ", "+format, args...)
			} else {
				fmt.Fprintf(w, format, args...)
				pstart = true
			}
		}

		w.WriteString("{")
		if r.GatewayInterface != "" {
			pr("GatewayInterface: %s", r.GatewayInterface)
		}
		if r.GatewayIdx > 0 {
			pr("GatewayIdx: %d", r.GatewayIdx)
		}
		if r.GatewayAddr != "" {
			pr("GatewayAddr: %s", r.GatewayAddr)
		}
		pr("Flags: %v", r.Flags)

		unknownFlags := r.RawFlags
		for fv := range flags {
			if r.RawFlags&fv == fv {
				unknownFlags &= ^fv
			}
		}
		if unknownFlags != 0 {
			pr("UnknownFlags: %x ", unknownFlags)
		}

		w.WriteString("}")
	}).Format(f, verb)
}

// ipFromRMAddr returns a netip.Addr converted from one of the
// route.Inet{4,6}Addr types.
func ipFromRMAddr(ifs map[int]netmon.Interface, addr any) netip.Addr {
	switch v := addr.(type) {
	case *route.Inet4Addr:
		return netip.AddrFrom4(v.IP)

	case *route.Inet6Addr:
		ip := netip.AddrFrom16(v.IP)
		if v.ZoneID != 0 {
			if iif, ok := ifs[v.ZoneID]; ok {
				ip = ip.WithZone(iif.Name)
			} else {
				ip = ip.WithZone(fmt.Sprint(v.ZoneID))
			}
		}

		return ip
	}

	return netip.Addr{}
}

// populateGateway populates gateway fields on a RouteEntry/RouteEntryBSD.
func populateGateway(re *RouteEntry, reSys *RouteEntryBSD, ifs map[int]netmon.Interface, addr any) {
	// If the address type has a valid IP, use that.
	if ip := ipFromRMAddr(ifs, addr); ip.IsValid() {
		re.Gateway = ip
		return
	}

	switch v := addr.(type) {
	case *route.LinkAddr:
		reSys.GatewayIdx = v.Index
		if iif, ok := ifs[v.Index]; ok {
			reSys.GatewayInterface = iif.Name
		}
		var sb strings.Builder
		for i, x := range v.Addr {
			if i != 0 {
				sb.WriteByte(':')
			}
			fmt.Fprintf(&sb, "%02x", x)
		}
		reSys.GatewayAddr = sb.String()
	}
}

// populateDestination populates the 'Dst' field on a RouteEntry based on the
// RouteMessage's destination and netmask fields.
func populateDestination(re *RouteEntry, ifs map[int]netmon.Interface, rm *route.RouteMessage) {
	dst := rm.Addrs[unix.RTAX_DST]
	if dst == nil {
		return
	}

	ip := ipFromRMAddr(ifs, dst)
	if !ip.IsValid() {
		return
	}

	if ip.Is4() {
		re.Family = 4
	} else {
		re.Family = 6
	}
	re.Dst = RouteDestination{
		Prefix: netip.PrefixFrom(ip, 32), // default if nothing more specific
	}

	// If the RTF_HOST flag is set, then this is a host route and there's
	// no netmask in this RouteMessage.
	if rm.Flags&unix.RTF_HOST != 0 {
		return
	}

	// As above if there's no netmask in the list of addrs
	if len(rm.Addrs) < unix.RTAX_NETMASK || rm.Addrs[unix.RTAX_NETMASK] == nil {
		return
	}

	nm := ipFromRMAddr(ifs, rm.Addrs[unix.RTAX_NETMASK])
	if !ip.IsValid() {
		return
	}

	// Count the number of bits in the netmask IP and use that to make our prefix.
	ones, _ /* bits */ := net.IPMask(nm.AsSlice()).Size()

	// Print this ourselves instead of using netip.Prefix so that we don't
	// lose the zone (since netip.Prefix strips that).
	//
	// NOTE(andrew): this doesn't print the same values as the 'netstat' tool
	// for some addresses on macOS, and I have no idea why. Specifically,
	// 'netstat -rn' will show something like:
	//    ff00::/8   ::1      UmCI     lo0
	//
	// But we will get:
	//    destination=ff00::/40 [...]
	//
	// The netmask that we get back from FetchRIB has 32 more bits in it
	// than netstat prints, but only for multicast routes.
	//
	// For consistency's sake, we're going to do the same here so that we
	// get the same values as netstat returns.
	if runtime.GOOS == "darwin" && ip.Is6() && ip.IsMulticast() && ones > 32 {
		ones -= 32
	}
	re.Dst = RouteDestination{
		Prefix: netip.PrefixFrom(ip, ones),
		Zone:   ip.Zone(),
	}
}

// routeEntryFromMsg returns a RouteEntry from a single route.Message
// returned by the operating system.
func routeEntryFromMsg(ifsByIdx map[int]netmon.Interface, msg route.Message) (RouteEntry, bool) {
	rm, ok := msg.(*route.RouteMessage)
	if !ok {
		return RouteEntry{}, false
	}

	// Ignore things that we don't understand
	if rm.Version < 3 || rm.Version > 5 {
		return RouteEntry{}, false
	}
	if rm.Type != rmExpectedType {
		return RouteEntry{}, false
	}
	if len(rm.Addrs) < unix.RTAX_GATEWAY {
		return RouteEntry{}, false
	}

	if rm.Flags&skipFlags != 0 {
		return RouteEntry{}, false
	}

	reSys := RouteEntryBSD{
		RawFlags: rm.Flags,
	}
	for fv, fs := range flags {
		if rm.Flags&fv == fv {
			reSys.Flags = append(reSys.Flags, fs)
		}
	}
	sort.Strings(reSys.Flags)

	re := RouteEntry{}
	hasFlag := func(f int) bool { return rm.Flags&f != 0 }
	switch {
	case hasFlag(unix.RTF_LOCAL):
		re.Type = RouteTypeLocal
	case hasFlag(unix.RTF_BROADCAST):
		re.Type = RouteTypeBroadcast
	case hasFlag(unix.RTF_MULTICAST):
		re.Type = RouteTypeMulticast

	// From the manpage: "host entry (net otherwise)"
	case !hasFlag(unix.RTF_HOST):
		re.Type = RouteTypeUnicast

	default:
		re.Type = RouteTypeOther
	}
	populateDestination(&re, ifsByIdx, rm)
	if unix.RTAX_GATEWAY < len(rm.Addrs) {
		populateGateway(&re, &reSys, ifsByIdx, rm.Addrs[unix.RTAX_GATEWAY])
	}

	if outif, ok := ifsByIdx[rm.Index]; ok {
		re.Interface = outif.Name
	}

	re.Sys = reSys
	return re, true
}

// Get returns route entries from the system route table, limited to at most
// 'max' results.
func Get(max int) ([]RouteEntry, error) {
	// Fetching the list of interfaces can race with fetching our route
	// table, but we do it anyway since it's helpful for debugging.
	ifs, err := netmon.GetInterfaceList()
	if err != nil {
		return nil, err
	}

	ifsByIdx := make(map[int]netmon.Interface)
	for _, iif := range ifs {
		ifsByIdx[iif.Index] = iif
	}

	rib, err := route.FetchRIB(syscall.AF_UNSPEC, ribType, 0)
	if err != nil {
		return nil, err
	}
	msgs, err := route.ParseRIB(parseType, rib)
	if err != nil {
		return nil, err
	}

	var ret []RouteEntry
	for _, m := range msgs {
		re, ok := routeEntryFromMsg(ifsByIdx, m)
		if ok {
			ret = append(ret, re)
			if len(ret) == max {
				break
			}
		}
	}
	return ret, nil
}
