sing-tun-lx/redirect_route_linux.go
2026-07-27 23:11:22 +08:00

244 lines
6.8 KiB
Go

//go:build linux
package tun
import (
"errors"
"math/rand"
"net"
"github.com/sagernet/netlink"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"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 E.Cause(err, "update interfaces")
}
redirectInterfaces := r.currentRedirectInterfaces()
r.redirectRouteAccess.Lock()
defer r.redirectRouteAccess.Unlock()
r.redirectRoutesActive = false
r.cleanupRedirectRoutesLocked()
defer func() {
if err != nil {
r.cleanupRedirectRoutesLocked()
}
}()
err = r.reconcileRedirectRoutesLocked(redirectInterfaces)
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 E.Cause(err, "add ipv4 redirect rule")
}
}
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 E.Cause(err, "add ipv6 redirect rule")
}
}
r.redirectRoutesActive = true
return nil
}
func (r *autoRedirect) currentRedirectInterfaces() []control.Interface {
tunName := r.tunOptions.Name
return common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
})
}
func redirectRouteAddressFamilies(iface control.Interface) (hasIPv4Address bool, hasIPv6Address bool) {
for _, prefix := range iface.Addresses {
address := prefix.Addr()
if address.Is4() {
hasIPv4Address = true
} else if address.Is6() && !address.Is4In6() {
hasIPv6Address = true
}
}
return
}
func (r *autoRedirect) updateRedirectRoutes() error {
err := r.interfaceFinder.Update()
if err != nil {
return E.Cause(err, "update interfaces")
}
redirectInterfaces := r.currentRedirectInterfaces()
r.redirectRouteAccess.Lock()
defer r.redirectRouteAccess.Unlock()
if !r.redirectRoutesActive {
return nil
}
return r.reconcileRedirectRoutesLocked(redirectInterfaces)
}
func (r *autoRedirect) cleanupRedirectRoutes() {
r.redirectRouteAccess.Lock()
defer r.redirectRouteAccess.Unlock()
r.redirectRoutesActive = false
r.cleanupRedirectRoutesLocked()
}
func (r *autoRedirect) cleanupRedirectRoutesLocked() {
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)
}
}
type redirectRouteKey struct {
linkIndex int
family int
}
func (r *autoRedirect) reconcileRedirectRoutesLocked(redirectInterfaces []control.Interface) error {
// Interface snapshots are not sufficient here: network managers can flush a
// route while a fast reconnect leaves the interface index and address families
// unchanged. Reconcile against the kernel's route table on every update.
currentRoutes, err := netlink.RouteListFiltered(netlink.FAMILY_ALL,
&netlink.Route{Table: r.redirectRouteTableIndex},
netlink.RT_FILTER_TABLE)
if err != nil {
return E.Cause(err, "list redirect routes")
}
routesToAdd, routesToDelete := calculateRedirectRouteChanges(
r.redirectRouteTableIndex,
redirectInterfaces,
currentRoutes,
r.enableIPv4,
r.enableIPv6,
)
for index := range routesToDelete {
route := &routesToDelete[index]
err = netlink.RouteDel(route)
if err != nil && !errors.Is(err, unix.ESRCH) && !errors.Is(err, unix.ENOENT) {
return E.Cause(err, "delete redirect route ", route)
}
}
for index := range routesToAdd {
route := &routesToAdd[index]
err = netlink.RouteAppend(route)
if err != nil {
return E.Cause(err, "append redirect route ", route)
}
}
return nil
}
func calculateRedirectRouteChanges(
tableIndex int,
redirectInterfaces []control.Interface,
currentRoutes []netlink.Route,
enableIPv4 bool,
enableIPv6 bool,
) (routesToAdd []netlink.Route, routesToDelete []netlink.Route) {
desiredRoutes := make(map[redirectRouteKey]struct{}, len(redirectInterfaces)*2)
for _, iface := range redirectInterfaces {
hasIPv4Address, hasIPv6Address := redirectRouteAddressFamilies(iface)
if enableIPv4 && hasIPv4Address {
desiredRoutes[redirectRouteKey{linkIndex: iface.Index, family: unix.AF_INET}] = struct{}{}
}
if enableIPv6 && hasIPv6Address {
desiredRoutes[redirectRouteKey{linkIndex: iface.Index, family: unix.AF_INET6}] = struct{}{}
}
}
for _, route := range currentRoutes {
key, isRedirectRoute := redirectRouteKeyFromRoute(route)
if !isRedirectRoute {
continue
}
if _, desired := desiredRoutes[key]; desired {
delete(desiredRoutes, key)
continue
}
routesToDelete = append(routesToDelete, route)
}
for key := range desiredRoutes {
routesToAdd = append(routesToAdd, newRedirectRoute(tableIndex, key))
}
return
}
func newRedirectRoute(tableIndex int, key redirectRouteKey) netlink.Route {
destination := net.IPv6loopback
if key.family == unix.AF_INET {
destination = net.IPv4(127, 0, 0, 1)
}
return netlink.Route{
LinkIndex: key.linkIndex,
Dst: netlink.NewIPNet(destination),
Table: tableIndex,
Type: unix.RTN_LOCAL,
Scope: netlink.SCOPE_HOST,
}
}
func redirectRouteKeyFromRoute(route netlink.Route) (redirectRouteKey, bool) {
if redirectRouteDestinationMatches(route.Dst, net.IPv4(127, 0, 0, 1), 32) {
return redirectRouteKey{linkIndex: route.LinkIndex, family: unix.AF_INET}, true
}
if redirectRouteDestinationMatches(route.Dst, net.IPv6loopback, 128) {
return redirectRouteKey{linkIndex: route.LinkIndex, family: unix.AF_INET6}, true
}
return redirectRouteKey{}, false
}
func redirectRouteDestinationMatches(destination *net.IPNet, address net.IP, prefixBits int) bool {
if destination == nil || !destination.IP.Equal(address) {
return false
}
ones, bits := destination.Mask.Size()
return ones == prefixBits && bits == prefixBits
}