mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-21 14:19:08 +02:00
[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.
This commit is contained in:
@@ -14,19 +14,51 @@ import (
|
||||
)
|
||||
|
||||
type Notifier struct {
|
||||
mu sync.Mutex
|
||||
currentPrefixes []string
|
||||
listener listener.NetworkChangeListener
|
||||
|
||||
listener listener.NetworkChangeListener
|
||||
listenerMux sync.Mutex
|
||||
// updates carries route snapshots to the single delivery goroutine. A
|
||||
// dedicated worker (rather than a fresh goroutine per notify) guarantees
|
||||
// the listener observes updates in the exact order they were produced.
|
||||
//
|
||||
// Without this ordering guarantee a stale snapshot can be delivered last
|
||||
// and clobber the correct one. On exit-node disable the ::/0 removal
|
||||
// arrives as a separate prefix update right after the 0.0.0.0/0 removal;
|
||||
// with a goroutine-per-notify the two could be reordered, leaving the
|
||||
// synthesized ::/0 default route installed on the tunnel and black-holing
|
||||
// all IPv6 traffic while IPv4 worked.
|
||||
updates chan string
|
||||
}
|
||||
|
||||
func NewNotifier() *Notifier {
|
||||
return &Notifier{}
|
||||
n := &Notifier{
|
||||
// Buffered so producers (route updates run under the route manager
|
||||
// lock) don't block on the listener callback. A small buffer absorbs
|
||||
// the bursts seen during exit-node toggles.
|
||||
updates: make(chan string, 16),
|
||||
}
|
||||
go n.deliverLoop()
|
||||
return n
|
||||
}
|
||||
|
||||
// deliverLoop is the single consumer of n.updates. Serializing delivery here
|
||||
// is what preserves ordering: snapshots reach the listener one at a time, in
|
||||
// production order.
|
||||
func (n *Notifier) deliverLoop() {
|
||||
for routes := range n.updates {
|
||||
n.mu.Lock()
|
||||
l := n.listener
|
||||
n.mu.Unlock()
|
||||
if l != nil {
|
||||
l.OnNetworkChanged(routes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Notifier) SetListener(listener listener.NetworkChangeListener) {
|
||||
n.listenerMux.Lock()
|
||||
defer n.listenerMux.Unlock()
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
n.listener = listener
|
||||
}
|
||||
|
||||
@@ -43,30 +75,25 @@ func (n *Notifier) OnNewRoutes(route.HAMap) {
|
||||
}
|
||||
|
||||
func (n *Notifier) OnNewPrefixes(prefixes []netip.Prefix) {
|
||||
newNets := make([]string, 0)
|
||||
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
|
||||
n.notify()
|
||||
}
|
||||
func (n *Notifier) notify() {
|
||||
n.listenerMux.Lock()
|
||||
defer n.listenerMux.Unlock()
|
||||
if n.listener == nil {
|
||||
return
|
||||
}
|
||||
// Snapshot the delivered string under the lock so it is consistent and
|
||||
// can't race with the next update mutating currentPrefixes.
|
||||
routes := strings.Join(n.currentPrefixes, ",")
|
||||
n.mu.Unlock()
|
||||
|
||||
go func(l listener.NetworkChangeListener) {
|
||||
l.OnNetworkChanged(strings.Join(n.currentPrefixes, ","))
|
||||
}(n.listener)
|
||||
n.updates <- routes
|
||||
}
|
||||
|
||||
func (n *Notifier) GetInitialRouteRanges() []string {
|
||||
|
||||
Reference in New Issue
Block a user