* [client] fix iOS route-update reordering that black-holed IPv6 on exit-node disable On iOS the route notifier delivered each prefix update from its own fire-and-forget goroutine (notify -> `go func`), so Go provided no ordering guarantee between consecutive updates. It also read currentPrefixes inside that goroutine without holding the lock, racing the next OnNewPrefixes write. On exit-node disable the core removes the default routes as two separate prefix updates (0.0.0.0/0, then the synthesized ::/0). When the two goroutines were reordered, the stale snapshot still containing ::/0 was delivered last and clobbered the correct default-free one. iOS then kept the ::/0 default route on the tunnel with no exit node to carry it, black-holing all IPv6 traffic while IPv4 recovered correctly. Fix: deliver updates through a single worker goroutine fed by a buffered channel, preserving production order, and snapshot the joined prefix string under the mutex so it can't race a concurrent update. Buffered so producers (which run under the route manager lock) don't block on the listener callback. * [client] close iOS notifier delivery goroutine on Stop, unbounded queue The delivery goroutine was never stopped, leaking on every engine restart. Add Notifier.Close, called from the route manager Stop after routing cleanup. Replace the buffered update channel with a cond-driven linked-list queue so route-update producers (running under the route manager lock) never block when the listener callback is slow.
126 lines
2.8 KiB
Go
126 lines
2.8 KiB
Go
//go:build android
|
|
|
|
package notifier
|
|
|
|
import (
|
|
"net/netip"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/listener"
|
|
"github.com/netbirdio/netbird/route"
|
|
)
|
|
|
|
type Notifier struct {
|
|
initialRoutes []*route.Route
|
|
currentRoutes []*route.Route
|
|
fakeIPRoutes []*route.Route
|
|
|
|
listener listener.NetworkChangeListener
|
|
listenerMux sync.Mutex
|
|
}
|
|
|
|
func NewNotifier() *Notifier {
|
|
return &Notifier{}
|
|
}
|
|
|
|
func (n *Notifier) SetListener(listener listener.NetworkChangeListener) {
|
|
n.listenerMux.Lock()
|
|
defer n.listenerMux.Unlock()
|
|
n.listener = listener
|
|
}
|
|
|
|
// SetInitialClientRoutes stores the initial route sets for TUN configuration.
|
|
func (n *Notifier) SetInitialClientRoutes(initialRoutes []*route.Route, routesForComparison []*route.Route) {
|
|
n.initialRoutes = filterStatic(initialRoutes)
|
|
n.currentRoutes = filterStatic(routesForComparison)
|
|
}
|
|
|
|
// SetFakeIPRoutes stores the fake IP routes to be included in every TUN rebuild.
|
|
func (n *Notifier) SetFakeIPRoutes(routes []*route.Route) {
|
|
n.fakeIPRoutes = routes
|
|
}
|
|
|
|
func (n *Notifier) OnNewRoutes(idMap route.HAMap) {
|
|
var newRoutes []*route.Route
|
|
for _, routes := range idMap {
|
|
for _, r := range routes {
|
|
if r.IsDynamic() {
|
|
continue
|
|
}
|
|
newRoutes = append(newRoutes, r)
|
|
}
|
|
}
|
|
|
|
if !n.hasRouteDiff(n.currentRoutes, newRoutes) {
|
|
return
|
|
}
|
|
|
|
n.currentRoutes = newRoutes
|
|
n.notify()
|
|
}
|
|
|
|
func (n *Notifier) OnNewPrefixes([]netip.Prefix) {
|
|
// Not used on Android
|
|
}
|
|
|
|
func (n *Notifier) notify() {
|
|
n.listenerMux.Lock()
|
|
defer n.listenerMux.Unlock()
|
|
if n.listener == nil {
|
|
return
|
|
}
|
|
|
|
allRoutes := slices.Clone(n.currentRoutes)
|
|
allRoutes = append(allRoutes, n.fakeIPRoutes...)
|
|
|
|
routeStrings := n.routesToStrings(allRoutes)
|
|
sort.Strings(routeStrings)
|
|
go func(l listener.NetworkChangeListener) {
|
|
l.OnNetworkChanged(strings.Join(routeStrings, ","))
|
|
}(n.listener)
|
|
}
|
|
|
|
func filterStatic(routes []*route.Route) []*route.Route {
|
|
out := make([]*route.Route, 0, len(routes))
|
|
for _, r := range routes {
|
|
if !r.IsDynamic() {
|
|
out = append(out, r)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (n *Notifier) routesToStrings(routes []*route.Route) []string {
|
|
nets := make([]string, 0, len(routes))
|
|
for _, r := range routes {
|
|
nets = append(nets, r.NetString())
|
|
}
|
|
return nets
|
|
}
|
|
|
|
func (n *Notifier) hasRouteDiff(a []*route.Route, b []*route.Route) bool {
|
|
slices.SortFunc(a, func(x, y *route.Route) int {
|
|
return strings.Compare(x.NetString(), y.NetString())
|
|
})
|
|
slices.SortFunc(b, func(x, y *route.Route) int {
|
|
return strings.Compare(x.NetString(), y.NetString())
|
|
})
|
|
|
|
return !slices.EqualFunc(a, b, func(x, y *route.Route) bool {
|
|
return x.NetString() == y.NetString()
|
|
})
|
|
}
|
|
|
|
func (n *Notifier) GetInitialRouteRanges() []string {
|
|
initialStrings := n.routesToStrings(n.initialRoutes)
|
|
sort.Strings(initialStrings)
|
|
return initialStrings
|
|
}
|
|
|
|
func (n *Notifier) Close() {
|
|
// unused
|
|
}
|