//go:build linux

package tun

import (
	"math/rand"
	"net"
	"net/netip"

	"github.com/sagernet/netlink"
	"github.com/sagernet/sing/common"
	"github.com/sagernet/sing/common/control"

	"golang.org/x/sys/unix"
)

const redirectRouteRulePriority = 1

func (r *autoRedirect) setupRedirectRoutes() error {
	for {
		r.redirectRouteTableIndex = int(rand.Uint32())
		if r.redirectRouteTableIndex == r.tunOptions.IPRoute2TableIndex {
			continue
		}
		routeList, fErr := netlink.RouteListFiltered(netlink.FAMILY_ALL,
			&netlink.Route{Table: r.redirectRouteTableIndex},
			netlink.RT_FILTER_TABLE)
		if len(routeList) == 0 || fErr != nil {
			break
		}
	}
	err := r.interfaceFinder.Update()
	if err != nil {
		return err
	}
	tunName := r.tunOptions.Name
	r.redirectInterfaces = common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
		return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
	})
	r.cleanupRedirectRoutes()
	for _, iface := range r.redirectInterfaces {
		err = r.addRedirectRoutes(iface)
		if err != nil {
			return err
		}
	}
	if r.enableIPv4 {
		rule := netlink.NewRule()
		rule.Priority = redirectRouteRulePriority
		rule.Table = r.redirectRouteTableIndex
		rule.Family = unix.AF_INET
		err = netlink.RuleAdd(rule)
		if err != nil {
			return err
		}
	}
	if r.enableIPv6 {
		rule := netlink.NewRule()
		rule.Priority = redirectRouteRulePriority
		rule.Table = r.redirectRouteTableIndex
		rule.Family = unix.AF_INET6
		err = netlink.RuleAdd(rule)
		if err != nil {
			return err
		}
	}
	return nil
}

func (r *autoRedirect) addRedirectRoutes(iface control.Interface) error {
	if r.enableIPv4 && common.Any(iface.Addresses, func(it netip.Prefix) bool {
		return it.Addr().Is4()
	}) {
		err := netlink.RouteAppend(&netlink.Route{
			LinkIndex: iface.Index,
			Dst:       &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(32, 32)},
			Table:     r.redirectRouteTableIndex,
			Type:      unix.RTN_LOCAL,
			Scope:     netlink.SCOPE_HOST,
		})
		if err != nil {
			return err
		}
	}
	if r.enableIPv6 && common.Any(iface.Addresses, func(it netip.Prefix) bool {
		return it.Addr().Is6() && !it.Addr().Is4In6()
	}) {
		err := netlink.RouteAppend(&netlink.Route{
			LinkIndex: iface.Index,
			Dst:       &net.IPNet{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)},
			Table:     r.redirectRouteTableIndex,
			Type:      unix.RTN_LOCAL,
			Scope:     netlink.SCOPE_HOST,
		})
		if err != nil {
			return err
		}
	}
	return nil
}

func (r *autoRedirect) removeRedirectRoutes(linkIndex int) {
	if r.enableIPv4 {
		_ = netlink.RouteDel(&netlink.Route{
			LinkIndex: linkIndex,
			Dst:       &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(32, 32)},
			Table:     r.redirectRouteTableIndex,
			Type:      unix.RTN_LOCAL,
		})
	}
	if r.enableIPv6 {
		_ = netlink.RouteDel(&netlink.Route{
			LinkIndex: linkIndex,
			Dst:       &net.IPNet{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)},
			Table:     r.redirectRouteTableIndex,
			Type:      unix.RTN_LOCAL,
		})
	}
}

func (r *autoRedirect) updateRedirectRoutes() error {
	err := r.interfaceFinder.Update()
	if err != nil {
		return err
	}
	tunName := r.tunOptions.Name
	newInterfaces := common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
		return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
	})
	oldMap := make(map[int]bool, len(r.redirectInterfaces))
	for _, iface := range r.redirectInterfaces {
		oldMap[iface.Index] = true
	}
	newMap := make(map[int]bool, len(newInterfaces))
	for _, iface := range newInterfaces {
		newMap[iface.Index] = true
	}
	for _, iface := range newInterfaces {
		if !oldMap[iface.Index] {
			err = r.addRedirectRoutes(iface)
			if err != nil {
				return err
			}
		}
	}
	for _, iface := range r.redirectInterfaces {
		if !newMap[iface.Index] {
			r.removeRedirectRoutes(iface.Index)
		}
	}
	r.redirectInterfaces = newInterfaces
	return nil
}

func (r *autoRedirect) cleanupRedirectRoutes() {
	if r.redirectRouteTableIndex == 0 {
		return
	}
	routes, _ := netlink.RouteListFiltered(netlink.FAMILY_ALL,
		&netlink.Route{Table: r.redirectRouteTableIndex},
		netlink.RT_FILTER_TABLE)
	for _, route := range routes {
		_ = netlink.RouteDel(&route)
	}
	if r.enableIPv4 {
		rule := netlink.NewRule()
		rule.Priority = redirectRouteRulePriority
		rule.Table = r.redirectRouteTableIndex
		rule.Family = unix.AF_INET
		_ = netlink.RuleDel(rule)
	}
	if r.enableIPv6 {
		rule := netlink.NewRule()
		rule.Priority = redirectRouteRulePriority
		rule.Table = r.redirectRouteTableIndex
		rule.Family = unix.AF_INET6
		_ = netlink.RuleDel(rule)
	}
}
