diff --git a/client/internal/engine.go b/client/internal/engine.go index f96451e54..f93d54772 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -257,6 +257,7 @@ type Engine struct { fileDrop *filedrop.Manager fileDropRunning bool fileDropPort uint16 + overlayWait overlayWaiter statusRecorder *peer.Status diff --git a/client/internal/engine_filedrop.go b/client/internal/engine_filedrop.go index a7ebd698c..81d94c887 100644 --- a/client/internal/engine_filedrop.go +++ b/client/internal/engine_filedrop.go @@ -15,6 +15,8 @@ import ( "github.com/netbirdio/netbird/client/internal/peer" ) +const fileDropWatchName = "the file drop receiver" + type filedropResolver struct { status *peer.Status } @@ -45,6 +47,12 @@ func (e *Engine) startFileDrop() { } wgAddr := e.wgInterface.Address() + if !e.overlayAddrReady(wgAddr.IP) { + log.Infof("file drop receiver waits for the overlay address %s", wgAddr.IP) + e.armOverlayWatch(fileDropWatchName, e.restartFileDrop) + return + } + addr := netip.AddrPortFrom(wgAddr.IP, fileDropListenPort()) resolver := filedropResolver{status: e.statusRecorder} @@ -63,6 +71,7 @@ func (e *Engine) startFileDrop() { if v6 := wgAddr.IPv6; v6.IsValid() { if err := e.fileDrop.AddReceiverListener(e.ctx, netip.AddrPortFrom(v6, bound)); err != nil { log.Warnf("failed to add IPv6 file drop listener: %v", err) + e.armOverlayWatch(fileDropWatchName, e.restartFileDrop) } } @@ -142,13 +151,19 @@ func (e *Engine) setFileDropTunnel() { e.fileDrop.SetTunnel(dial, e.statusRecorder.GetLocalPeerState().FQDN) } -// restartFileDrop rebinds the receiver after the platform replaced the tunnel -// device. The listeners are bound to the overlay address of the interface being -// swapped out and do not survive it: Android renews the tun on every route -// change, which leaves the IPv4 listener dead with accept4: invalid argument. -// No-op when it is not running. See Engine.rebindOverlayListeners. +// restartFileDrop gives the receiver listeners on the interface as it is now. +// The listeners are bound to the overlay address of the interface being swapped +// out and do not survive it: Android renews the tun on every route change, which +// leaves the IPv4 listener dead with accept4: invalid argument. A receiver that +// is not running is started rather than skipped, since the reason it is down may +// be the very bind this rebind can now make. See Engine.rebindOverlayListeners. func (e *Engine) restartFileDrop() error { - if e.fileDrop == nil || !e.fileDropRunning || e.wgInterface == nil { + if e.fileDrop == nil || e.wgInterface == nil { + return nil + } + + if !e.fileDropRunning { + e.startFileDrop() return nil } diff --git a/client/internal/engine_overlay_wait.go b/client/internal/engine_overlay_wait.go new file mode 100644 index 000000000..285ed3736 --- /dev/null +++ b/client/internal/engine_overlay_wait.go @@ -0,0 +1,20 @@ +//go:build !ios + +package internal + +import "net/netip" + +// overlayWaiter carries no state off iOS. Every other platform assigns the +// overlay address in the same call chain that creates the interface, or hands +// the engine an interface that already carries it, so a listener bound right +// after has nothing to wait for. See the iOS variant for what the wait is. +type overlayWaiter struct{} + +// overlayAddrReady reports whether ip can be bound. Always true here. +func (e *Engine) overlayAddrReady(netip.Addr) bool { + return true +} + +// armOverlayWatch has nothing to watch here and is never reached, since +// overlayAddrReady never refuses. +func (e *Engine) armOverlayWatch(string, overlayRebind) {} diff --git a/client/internal/engine_overlay_wait_ios.go b/client/internal/engine_overlay_wait_ios.go new file mode 100644 index 000000000..f13bceea2 --- /dev/null +++ b/client/internal/engine_overlay_wait_ios.go @@ -0,0 +1,136 @@ +//go:build ios + +package internal + +import ( + "maps" + "net" + "net/netip" + "slices" + "strings" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + overlayWatchInterval = 200 * time.Millisecond + overlayWatchTimeout = time.Minute +) + +// overlayWaiter holds what waits for the overlay address, keyed by name so a +// second arming of the same subsystem replaces it. Guarded by syncMsgMux. +type overlayWaiter struct { + watching bool + waiters map[string]overlayRebind +} + +// overlayAddrReady reports whether ip is on the tunnel interface as it is now. +// +// The engine learning its overlay address is not the same as the address being +// usable. iOS does not let the client touch the interface: it hands the engine +// a tun fd and applies the address itself, out of band and without telling the +// engine when it lands. A bind placed in between fails with EADDRNOTAVAIL, and +// the listener has no way back on its own. The caller must hold syncMsgMux. +func (e *Engine) overlayAddrReady(ip netip.Addr) bool { + if e.wgInterface == nil || !ip.IsValid() { + return false + } + + iface, err := net.InterfaceByName(e.wgInterface.Name()) + if err != nil { + log.Debugf("look up tunnel interface %s: %v", e.wgInterface.Name(), err) + return false + } + addrs, err := iface.Addrs() + if err != nil { + log.Debugf("list addresses of %s: %v", e.wgInterface.Name(), err) + return false + } + + for _, addr := range addrs { + prefix, err := netip.ParsePrefix(addr.String()) + if err != nil { + continue + } + if prefix.Addr().Unmap() == ip.Unmap() { + return true + } + } + return false +} + +// armOverlayWatch waits for the overlay address to appear and then binds what +// asked to be bound. +// +// Only the subsystems that armed the watch are bound. This is deliberately +// narrower than rebindOverlayListeners: an address that was missing says +// nothing about the listeners that did come up, and rebuilding those would drop +// the SSH sessions and DNS queries they are carrying. The caller must hold +// syncMsgMux. +func (e *Engine) armOverlayWatch(name string, ensure overlayRebind) { + if e.ctx.Err() != nil { + return + } + + if e.overlayWait.waiters == nil { + e.overlayWait.waiters = make(map[string]overlayRebind) + } + e.overlayWait.waiters[name] = ensure + + if e.overlayWait.watching { + return + } + e.overlayWait.watching = true + + e.shutdownWg.Add(1) + go func() { + defer e.shutdownWg.Done() + + ticker := time.NewTicker(overlayWatchInterval) + defer ticker.Stop() + deadline := time.NewTimer(overlayWatchTimeout) + defer deadline.Stop() + + for { + select { + case <-e.ctx.Done(): + return + case <-deadline.C: + e.syncMsgMux.Lock() + waiting := slices.Sorted(maps.Keys(e.overlayWait.waiters)) + e.overlayWait = overlayWaiter{} + e.syncMsgMux.Unlock() + log.Errorf("overlay address did not appear within %s, still down: %s", + overlayWatchTimeout, strings.Join(waiting, ", ")) + return + case <-ticker.C: + if e.bindOverlayWaiters() { + return + } + } + } + }() +} + +// bindOverlayWaiters runs what the watch collected once the overlay address is +// up, reporting whether the wait is over. +func (e *Engine) bindOverlayWaiters() bool { + e.syncMsgMux.Lock() + defer e.syncMsgMux.Unlock() + + if e.wgInterface == nil || !e.overlayAddrReady(e.wgInterface.Address().IP) { + return false + } + + waiters := e.overlayWait.waiters + e.overlayWait = overlayWaiter{} + + for name, ensure := range waiters { + log.Infof("overlay address is up, binding %s", name) + if err := ensure(); err != nil { + log.Errorf("bind %s after the overlay address came up: %v", name, err) + } + } + return true +}