Fix redirect routes
This commit is contained in:
parent
eccdcee073
commit
3a09076491
3 changed files with 176 additions and 106 deletions
|
|
@ -17,6 +17,7 @@ import (
|
||||||
type networkUpdateMonitor struct {
|
type networkUpdateMonitor struct {
|
||||||
routeUpdate chan netlink.RouteUpdate
|
routeUpdate chan netlink.RouteUpdate
|
||||||
linkUpdate chan netlink.LinkUpdate
|
linkUpdate chan netlink.LinkUpdate
|
||||||
|
addressUpdate chan netlink.AddrUpdate
|
||||||
close chan struct{}
|
close chan struct{}
|
||||||
|
|
||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
|
|
@ -34,6 +35,7 @@ func NewNetworkUpdateMonitor(logger logger.Logger) (NetworkUpdateMonitor, error)
|
||||||
monitor := &networkUpdateMonitor{
|
monitor := &networkUpdateMonitor{
|
||||||
routeUpdate: make(chan netlink.RouteUpdate, 2),
|
routeUpdate: make(chan netlink.RouteUpdate, 2),
|
||||||
linkUpdate: make(chan netlink.LinkUpdate, 2),
|
linkUpdate: make(chan netlink.LinkUpdate, 2),
|
||||||
|
addressUpdate: make(chan netlink.AddrUpdate, 2),
|
||||||
close: make(chan struct{}),
|
close: make(chan struct{}),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
|
|
@ -63,28 +65,46 @@ func (m *networkUpdateMonitor) Start() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "subscribe link updates")
|
return E.Cause(err, "subscribe link updates")
|
||||||
}
|
}
|
||||||
go m.loopUpdate()
|
err = netlink.AddrSubscribe(m.addressUpdate, m.close)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "subscribe address updates")
|
||||||
|
}
|
||||||
|
go m.loopUpdate(time.Second)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *networkUpdateMonitor) loopUpdate() {
|
func (m *networkUpdateMonitor) loopUpdate(minDuration time.Duration) {
|
||||||
const minDuration = time.Second
|
|
||||||
timer := time.NewTimer(minDuration)
|
timer := time.NewTimer(minDuration)
|
||||||
|
timer.Stop()
|
||||||
defer timer.Stop()
|
defer timer.Stop()
|
||||||
|
var (
|
||||||
|
timerC <-chan time.Time
|
||||||
|
pending bool
|
||||||
|
)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-m.close:
|
case <-m.close:
|
||||||
return
|
return
|
||||||
case <-m.routeUpdate:
|
case <-m.routeUpdate:
|
||||||
case <-m.linkUpdate:
|
case <-m.linkUpdate:
|
||||||
|
case <-m.addressUpdate:
|
||||||
|
case <-timerC:
|
||||||
|
if pending {
|
||||||
|
m.emit()
|
||||||
|
pending = false
|
||||||
|
timer.Reset(minDuration)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
timerC = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if timerC != nil {
|
||||||
|
pending = true
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
m.emit()
|
m.emit()
|
||||||
select {
|
|
||||||
case <-m.close:
|
|
||||||
return
|
|
||||||
case <-timer.C:
|
|
||||||
timer.Reset(minDuration)
|
timer.Reset(minDuration)
|
||||||
}
|
timerC = timer.C
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/sagernet/nftables"
|
"github.com/sagernet/nftables"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
|
|
@ -43,7 +44,8 @@ type autoRedirect struct {
|
||||||
nfqueueHandler *nfqueueHandler
|
nfqueueHandler *nfqueueHandler
|
||||||
nfqueueEnabled bool
|
nfqueueEnabled bool
|
||||||
redirectRouteTableIndex int
|
redirectRouteTableIndex int
|
||||||
redirectInterfaces []control.Interface
|
redirectRouteAccess sync.Mutex
|
||||||
|
redirectRoutesActive bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
|
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
|
||||||
|
|
@ -179,8 +181,8 @@ func (r *autoRedirect) Close() error {
|
||||||
r.nfqueueHandler.Close()
|
r.nfqueueHandler.Close()
|
||||||
}
|
}
|
||||||
if r.useNFTables {
|
if r.useNFTables {
|
||||||
r.cleanupRedirectRoutes()
|
|
||||||
r.cleanupNFTables()
|
r.cleanupNFTables()
|
||||||
|
r.cleanupRedirectRoutes()
|
||||||
} else {
|
} else {
|
||||||
r.cleanupIPTables()
|
r.cleanupIPTables()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
package tun
|
package tun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
|
@ -33,16 +34,19 @@ func (r *autoRedirect) setupRedirectRoutes() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "update interfaces")
|
return E.Cause(err, "update interfaces")
|
||||||
}
|
}
|
||||||
tunName := r.tunOptions.Name
|
redirectInterfaces := r.currentRedirectInterfaces()
|
||||||
r.redirectInterfaces = common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
|
r.redirectRouteAccess.Lock()
|
||||||
return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
|
defer r.redirectRouteAccess.Unlock()
|
||||||
})
|
r.redirectRoutesActive = false
|
||||||
r.cleanupRedirectRoutes()
|
r.cleanupRedirectRoutesLocked()
|
||||||
for _, iface := range r.redirectInterfaces {
|
defer func() {
|
||||||
err = r.addRedirectRoutes(iface)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "add redirect routes for ", iface.Name)
|
r.cleanupRedirectRoutesLocked()
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
err = r.reconcileRedirectRoutesLocked(redirectInterfaces)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
if r.enableIPv4 {
|
if r.enableIPv4 {
|
||||||
rule := netlink.NewRule()
|
rule := netlink.NewRule()
|
||||||
|
|
@ -64,36 +68,15 @@ func (r *autoRedirect) setupRedirectRoutes() error {
|
||||||
return E.Cause(err, "add ipv6 redirect rule")
|
return E.Cause(err, "add ipv6 redirect rule")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
r.redirectRoutesActive = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *autoRedirect) addRedirectRoutes(iface control.Interface) error {
|
func (r *autoRedirect) currentRedirectInterfaces() []control.Interface {
|
||||||
hasIPv4Address, hasIPv6Address := redirectRouteAddressFamilies(iface)
|
tunName := r.tunOptions.Name
|
||||||
if r.enableIPv4 && hasIPv4Address {
|
return common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
|
||||||
err := netlink.RouteAppend(&netlink.Route{
|
return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
|
||||||
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 E.Cause(err, "append ipv4 loopback route")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if r.enableIPv6 && hasIPv6Address {
|
|
||||||
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 E.Cause(err, "append ipv6 loopback route")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func redirectRouteAddressFamilies(iface control.Interface) (hasIPv4Address bool, hasIPv6Address bool) {
|
func redirectRouteAddressFamilies(iface control.Interface) (hasIPv4Address bool, hasIPv6Address bool) {
|
||||||
|
|
@ -108,68 +91,28 @@ func redirectRouteAddressFamilies(iface control.Interface) (hasIPv4Address bool,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
func (r *autoRedirect) updateRedirectRoutes() error {
|
||||||
err := r.interfaceFinder.Update()
|
err := r.interfaceFinder.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "update interfaces")
|
return E.Cause(err, "update interfaces")
|
||||||
}
|
}
|
||||||
tunName := r.tunOptions.Name
|
redirectInterfaces := r.currentRedirectInterfaces()
|
||||||
newInterfaces := common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
|
r.redirectRouteAccess.Lock()
|
||||||
return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
|
defer r.redirectRouteAccess.Unlock()
|
||||||
})
|
if !r.redirectRoutesActive {
|
||||||
oldMap := make(map[int]control.Interface, len(r.redirectInterfaces))
|
|
||||||
for _, iface := range r.redirectInterfaces {
|
|
||||||
oldMap[iface.Index] = iface
|
|
||||||
}
|
|
||||||
newMap := make(map[int]bool, len(newInterfaces))
|
|
||||||
for _, iface := range newInterfaces {
|
|
||||||
newMap[iface.Index] = true
|
|
||||||
}
|
|
||||||
for _, iface := range newInterfaces {
|
|
||||||
oldInterface, loaded := oldMap[iface.Index]
|
|
||||||
if loaded {
|
|
||||||
oldHasIPv4Address, oldHasIPv6Address := redirectRouteAddressFamilies(oldInterface)
|
|
||||||
hasIPv4Address, hasIPv6Address := redirectRouteAddressFamilies(iface)
|
|
||||||
if (!r.enableIPv4 || oldHasIPv4Address == hasIPv4Address) &&
|
|
||||||
(!r.enableIPv6 || oldHasIPv6Address == hasIPv6Address) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r.removeRedirectRoutes(iface.Index)
|
|
||||||
}
|
|
||||||
err = r.addRedirectRoutes(iface)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "add redirect routes for ", iface.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, iface := range r.redirectInterfaces {
|
|
||||||
if !newMap[iface.Index] {
|
|
||||||
r.removeRedirectRoutes(iface.Index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.redirectInterfaces = newInterfaces
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
return r.reconcileRedirectRoutesLocked(redirectInterfaces)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *autoRedirect) cleanupRedirectRoutes() {
|
func (r *autoRedirect) cleanupRedirectRoutes() {
|
||||||
|
r.redirectRouteAccess.Lock()
|
||||||
|
defer r.redirectRouteAccess.Unlock()
|
||||||
|
r.redirectRoutesActive = false
|
||||||
|
r.cleanupRedirectRoutesLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) cleanupRedirectRoutesLocked() {
|
||||||
if r.redirectRouteTableIndex == 0 {
|
if r.redirectRouteTableIndex == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -194,3 +137,108 @@ func (r *autoRedirect) cleanupRedirectRoutes() {
|
||||||
_ = netlink.RuleDel(rule)
|
_ = 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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue