* [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.
103 lines
1.8 KiB
Go
103 lines
1.8 KiB
Go
//go:build ios
|
|
|
|
package notifier
|
|
|
|
import (
|
|
"container/list"
|
|
"net/netip"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/listener"
|
|
"github.com/netbirdio/netbird/route"
|
|
)
|
|
|
|
type Notifier struct {
|
|
mu sync.Mutex
|
|
cond *sync.Cond
|
|
currentPrefixes []string
|
|
listener listener.NetworkChangeListener
|
|
queue *list.List
|
|
closed bool
|
|
}
|
|
|
|
func NewNotifier() *Notifier {
|
|
n := &Notifier{
|
|
queue: list.New(),
|
|
}
|
|
n.cond = sync.NewCond(&n.mu)
|
|
go n.deliverLoop()
|
|
return n
|
|
}
|
|
|
|
func (n *Notifier) SetListener(listener listener.NetworkChangeListener) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
n.listener = listener
|
|
}
|
|
|
|
func (n *Notifier) SetInitialClientRoutes([]*route.Route, []*route.Route) {
|
|
// iOS doesn't care about initial routes
|
|
}
|
|
|
|
func (n *Notifier) SetFakeIPRoutes([]*route.Route) {
|
|
// Not used on iOS
|
|
}
|
|
|
|
func (n *Notifier) OnNewRoutes(route.HAMap) {
|
|
// Not used on iOS
|
|
}
|
|
|
|
func (n *Notifier) OnNewPrefixes(prefixes []netip.Prefix) {
|
|
newNets := make([]string, 0, len(prefixes))
|
|
for _, prefix := range prefixes {
|
|
newNets = append(newNets, prefix.String())
|
|
}
|
|
|
|
sort.Strings(newNets)
|
|
|
|
n.mu.Lock()
|
|
if slices.Equal(n.currentPrefixes, newNets) {
|
|
n.mu.Unlock()
|
|
return
|
|
}
|
|
n.currentPrefixes = newNets
|
|
routes := strings.Join(n.currentPrefixes, ",")
|
|
n.queue.PushBack(routes)
|
|
n.cond.Signal()
|
|
n.mu.Unlock()
|
|
}
|
|
|
|
func (n *Notifier) Close() {
|
|
n.mu.Lock()
|
|
n.closed = true
|
|
n.cond.Signal()
|
|
n.mu.Unlock()
|
|
}
|
|
|
|
func (n *Notifier) GetInitialRouteRanges() []string {
|
|
return nil
|
|
}
|
|
|
|
func (n *Notifier) deliverLoop() {
|
|
for {
|
|
n.mu.Lock()
|
|
for n.queue.Len() == 0 && !n.closed {
|
|
n.cond.Wait()
|
|
}
|
|
if n.closed && n.queue.Len() == 0 {
|
|
n.mu.Unlock()
|
|
return
|
|
}
|
|
routes := n.queue.Remove(n.queue.Front()).(string)
|
|
l := n.listener
|
|
n.mu.Unlock()
|
|
|
|
if l != nil {
|
|
l.OnNetworkChanged(routes)
|
|
}
|
|
}
|
|
}
|