diff --git a/redirect_linux.go b/redirect_linux.go index 5bea0c4..237b02a 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -19,29 +19,31 @@ import ( ) type autoRedirect struct { - tunOptions *Options - ctx context.Context - handler Handler - logger logger.Logger - tableName string - networkMonitor NetworkUpdateMonitor - networkListener *list.Element[NetworkUpdateCallback] - interfaceFinder control.InterfaceFinder - localAddresses []netip.Prefix - customRedirectPortFunc func() int - customRedirectPort int - redirectServer *redirectServer - enableIPv4 bool - enableIPv6 bool - iptablesPath string - ip6tablesPath string - useNFTables bool - androidSu bool - suPath string - routeAddressSet *[]*netipx.IPSet - routeExcludeAddressSet *[]*netipx.IPSet - nfqueueHandler *nfqueueHandler - nfqueueEnabled bool + tunOptions *Options + ctx context.Context + handler Handler + logger logger.Logger + tableName string + networkMonitor NetworkUpdateMonitor + networkListener *list.Element[NetworkUpdateCallback] + interfaceFinder control.InterfaceFinder + localAddresses []netip.Prefix + customRedirectPortFunc func() int + customRedirectPort int + redirectServer *redirectServer + enableIPv4 bool + enableIPv6 bool + iptablesPath string + ip6tablesPath string + useNFTables bool + androidSu bool + suPath string + routeAddressSet *[]*netipx.IPSet + routeExcludeAddressSet *[]*netipx.IPSet + nfqueueHandler *nfqueueHandler + nfqueueEnabled bool + redirectRouteTableIndex int + redirectInterfaces []control.Interface } func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { @@ -152,6 +154,12 @@ func (r *autoRedirect) Start() error { } r.cleanupNFTables() err = r.setupNFTables() + if err == nil && r.tunOptions.AutoRedirectMarkMode { + err = r.setupRedirectRoutes() + if err != nil { + r.cleanupNFTables() + } + } } else { r.cleanupIPTables() err = r.setupIPTables() @@ -164,6 +172,7 @@ func (r *autoRedirect) Close() error { r.nfqueueHandler.Close() } if r.useNFTables { + r.cleanupRedirectRoutes() r.cleanupNFTables() } else { r.cleanupIPTables() diff --git a/redirect_nftables.go b/redirect_nftables.go index 3154101..3da8576 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -293,6 +293,12 @@ func (r *autoRedirect) setupNFTables() error { if err != nil { r.logger.Error("update local address set: ", err) } + if r.tunOptions.AutoRedirectMarkMode { + err = r.updateRedirectRoutes() + if err != nil { + r.logger.Error("update redirect routes: ", err) + } + } }) return nil } diff --git a/redirect_route_linux.go b/redirect_route_linux.go new file mode 100644 index 0000000..726ec1d --- /dev/null +++ b/redirect_route_linux.go @@ -0,0 +1,174 @@ +//go:build linux + +package tun + +import ( + "math/rand" + "net" + + "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.Index) + 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(linkIndex int) error { + if r.enableIPv4 { + err := netlink.RouteAppend(&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, + Scope: netlink.SCOPE_HOST, + }) + if err != nil { + return err + } + } + if r.enableIPv6 { + err := netlink.RouteAppend(&netlink.Route{ + LinkIndex: linkIndex, + 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.Index) + 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) + } +}