From 64bb6db7d35a082ece604d386025cce4f50eb646 Mon Sep 17 00:00:00 2001 From: Hakan Sariman Date: Tue, 19 Aug 2025 12:09:04 +0300 Subject: [PATCH 1/6] [client] Add WireGuard interface lifecycle monitoring Implement a background watcher that restarts the engine when the WireGuard interface is created or deleted. This addition enhances the engine's resilience to external changes in the interface state. --- client/internal/engine.go | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/client/internal/engine.go b/client/internal/engine.go index 197036ea9cc..4b1f02be8ba 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -464,6 +464,9 @@ func (e *Engine) Start() error { // starting network monitor at the very last to avoid disruptions e.startNetworkMonitor() + + // monitor WireGuard interface lifecycle and restart engine on changes + e.startWGIfaceMonitor() return nil } @@ -1701,6 +1704,63 @@ func (e *Engine) startNetworkMonitor() { }() } +// startWGIfaceMonitor starts a background watcher that restarts the engine if +// the WireGuard interface is created or deleted externally while the engine is running. +// It relies on the engine context cancellation to stop. +func (e *Engine) startWGIfaceMonitor() { + // wgInterface should be initialized at this point + if e.wgInterface == nil { + return + } + + name := e.wgInterface.Name() + if name == "" { + return + } + + // Polling approach for cross-platform simplicity + go func(ctx context.Context, ifaceName string) { + log.Infof("Interface monitor: watching %s", ifaceName) + + // Determine initial state + prevExists := interfaceExists(ifaceName) + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + log.Infof("Interface monitor: stopped for %s", ifaceName) + return + case <-ticker.C: + exists := interfaceExists(ifaceName) + if exists != prevExists { + state := "deleted" + if exists { + state = "created" + } + log.Infof("Interface monitor: %s %s, restarting engine", ifaceName, state) + e.restartEngine() + return + } + } + } + }(e.ctx, name) +} + +// interfaceExists checks whether a network interface with the given name exists. +func interfaceExists(name string) bool { + if name == "" { + return false + } + ifi, err := net.InterfaceByName(name) + if err != nil || ifi == nil { + return false + } + return true +} + func (e *Engine) addrViaRoutes(addr netip.Addr) (bool, netip.Prefix, error) { var vpnRoutes []netip.Prefix for _, routes := range e.routeManager.GetClientRoutes() { From 6016dbd8707e65b545239d467ebfb02bb394ab6a Mon Sep 17 00:00:00 2001 From: Hakan Sariman Date: Fri, 22 Aug 2025 08:10:33 +0300 Subject: [PATCH 2/6] [client] Enhance WireGuard interface monitoring Refine the background watcher to handle interface lifecycle changes more accurately. The monitor now skips execution on mobile platforms and tracks interface recreation by comparing indices, improving the engine's response to external interface modifications. --- client/internal/engine.go | 64 +++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/client/internal/engine.go b/client/internal/engine.go index 4b1f02be8ba..5571dcee9d9 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1705,9 +1705,15 @@ func (e *Engine) startNetworkMonitor() { } // startWGIfaceMonitor starts a background watcher that restarts the engine if -// the WireGuard interface is created or deleted externally while the engine is running. +// the WireGuard interface is deleted externally while the engine is running. // It relies on the engine context cancellation to stop. func (e *Engine) startWGIfaceMonitor() { + // Skip on mobile platforms as they handle interface lifecycle differently + if runtime.GOOS == "android" || runtime.GOOS == "ios" { + log.Debugf("Interface monitor: skipped on %s platform", runtime.GOOS) + return + } + // wgInterface should be initialized at this point if e.wgInterface == nil { return @@ -1718,12 +1724,16 @@ func (e *Engine) startWGIfaceMonitor() { return } - // Polling approach for cross-platform simplicity - go func(ctx context.Context, ifaceName string) { - log.Infof("Interface monitor: watching %s", ifaceName) + // Get initial interface index to track the specific interface instance + initialIndex, err := getInterfaceIndex(name) + if err != nil { + log.Debugf("Interface monitor: interface %s not found, skipping monitor", name) + return + } - // Determine initial state - prevExists := interfaceExists(ifaceName) + // Polling approach for cross-platform simplicity + go func(ctx context.Context, ifaceName string, expectedIndex int) { + log.Infof("Interface monitor: watching %s (index: %d)", ifaceName, expectedIndex) ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() @@ -1734,31 +1744,45 @@ func (e *Engine) startWGIfaceMonitor() { log.Infof("Interface monitor: stopped for %s", ifaceName) return case <-ticker.C: - exists := interfaceExists(ifaceName) - if exists != prevExists { - state := "deleted" - if exists { - state = "created" - } - log.Infof("Interface monitor: %s %s, restarting engine", ifaceName, state) + currentIndex, err := getInterfaceIndex(ifaceName) + if err != nil { + // Interface was deleted + log.Infof("Interface monitor: %s deleted, restarting engine", ifaceName) + e.restartEngine() + return + } + + // Check if interface index changed (interface was recreated) + if currentIndex != expectedIndex { + log.Infof("Interface monitor: %s recreated (index changed from %d to %d), restarting engine", + ifaceName, expectedIndex, currentIndex) e.restartEngine() return } } } - }(e.ctx, name) + }(e.ctx, name, initialIndex) } -// interfaceExists checks whether a network interface with the given name exists. -func interfaceExists(name string) bool { +// getInterfaceIndex returns the index of a network interface by name. +// Returns an error if the interface is not found. +func getInterfaceIndex(name string) (int, error) { if name == "" { - return false + return 0, fmt.Errorf("empty interface name") } ifi, err := net.InterfaceByName(name) - if err != nil || ifi == nil { - return false + if err != nil { + // Check if it's specifically a "not found" error + if errors.Is(err, &net.OpError{}) { + // On some systems, this might be a "not found" error + return 0, fmt.Errorf("interface not found: %w", err) + } + return 0, fmt.Errorf("failed to lookup interface: %w", err) } - return true + if ifi == nil { + return 0, fmt.Errorf("interface not found") + } + return ifi.Index, nil } func (e *Engine) addrViaRoutes(addr netip.Addr) (bool, netip.Prefix, error) { From 88f98a03e7f42974e6aa398db55ce181cda777bb Mon Sep 17 00:00:00 2001 From: Hakan Sariman Date: Mon, 8 Sep 2025 11:20:02 +0700 Subject: [PATCH 3/6] [client] Introduce WGIfaceMonitor for enhanced WireGuard interface management Add a new WGIfaceMonitor to manage the WireGuard interface lifecycle, allowing the engine to respond to interface deletions and recreations. This implementation replaces the previous monitoring method, improving reliability and maintaining compatibility with mobile platforms. --- client/internal/engine.go | 92 +++------------------ client/internal/wg_iface_monitor.go | 124 ++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 81 deletions(-) create mode 100644 client/internal/wg_iface_monitor.go diff --git a/client/internal/engine.go b/client/internal/engine.go index 5571dcee9d9..9b62b1a6cd2 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -194,6 +194,9 @@ type Engine struct { latestSyncResponse *mgmProto.SyncResponse connSemaphore *semaphoregroup.SemaphoreGroup flowManager nftypes.FlowManager + + // WireGuard interface monitor + wgIfaceMonitor *WGIfaceMonitor } // Peer is an instance of the Connection Peer @@ -302,6 +305,12 @@ func (e *Engine) Stop() error { e.srWatcher.Close() } + // Stop WireGuard interface monitor and wait for it to exit + if e.wgIfaceMonitor != nil { + e.wgIfaceMonitor.Stop() + e.wgIfaceMonitor = nil + } + e.statusRecorder.ReplaceOfflinePeers([]peer.State{}) e.statusRecorder.UpdateDNSStates([]peer.NSGroupState{}) e.statusRecorder.UpdateRelayStates([]relay.ProbeResult{}) @@ -466,7 +475,8 @@ func (e *Engine) Start() error { e.startNetworkMonitor() // monitor WireGuard interface lifecycle and restart engine on changes - e.startWGIfaceMonitor() + e.wgIfaceMonitor = NewWGIfaceMonitor(e.restartEngine) + e.wgIfaceMonitor.Start(e.wgInterface.Name()) return nil } @@ -1704,86 +1714,6 @@ func (e *Engine) startNetworkMonitor() { }() } -// startWGIfaceMonitor starts a background watcher that restarts the engine if -// the WireGuard interface is deleted externally while the engine is running. -// It relies on the engine context cancellation to stop. -func (e *Engine) startWGIfaceMonitor() { - // Skip on mobile platforms as they handle interface lifecycle differently - if runtime.GOOS == "android" || runtime.GOOS == "ios" { - log.Debugf("Interface monitor: skipped on %s platform", runtime.GOOS) - return - } - - // wgInterface should be initialized at this point - if e.wgInterface == nil { - return - } - - name := e.wgInterface.Name() - if name == "" { - return - } - - // Get initial interface index to track the specific interface instance - initialIndex, err := getInterfaceIndex(name) - if err != nil { - log.Debugf("Interface monitor: interface %s not found, skipping monitor", name) - return - } - - // Polling approach for cross-platform simplicity - go func(ctx context.Context, ifaceName string, expectedIndex int) { - log.Infof("Interface monitor: watching %s (index: %d)", ifaceName, expectedIndex) - - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - log.Infof("Interface monitor: stopped for %s", ifaceName) - return - case <-ticker.C: - currentIndex, err := getInterfaceIndex(ifaceName) - if err != nil { - // Interface was deleted - log.Infof("Interface monitor: %s deleted, restarting engine", ifaceName) - e.restartEngine() - return - } - - // Check if interface index changed (interface was recreated) - if currentIndex != expectedIndex { - log.Infof("Interface monitor: %s recreated (index changed from %d to %d), restarting engine", - ifaceName, expectedIndex, currentIndex) - e.restartEngine() - return - } - } - } - }(e.ctx, name, initialIndex) -} - -// getInterfaceIndex returns the index of a network interface by name. -// Returns an error if the interface is not found. -func getInterfaceIndex(name string) (int, error) { - if name == "" { - return 0, fmt.Errorf("empty interface name") - } - ifi, err := net.InterfaceByName(name) - if err != nil { - // Check if it's specifically a "not found" error - if errors.Is(err, &net.OpError{}) { - // On some systems, this might be a "not found" error - return 0, fmt.Errorf("interface not found: %w", err) - } - return 0, fmt.Errorf("failed to lookup interface: %w", err) - } - if ifi == nil { - return 0, fmt.Errorf("interface not found") - } - return ifi.Index, nil -} func (e *Engine) addrViaRoutes(addr netip.Addr) (bool, netip.Prefix, error) { var vpnRoutes []netip.Prefix diff --git a/client/internal/wg_iface_monitor.go b/client/internal/wg_iface_monitor.go new file mode 100644 index 00000000000..952b8327bc7 --- /dev/null +++ b/client/internal/wg_iface_monitor.go @@ -0,0 +1,124 @@ +package internal + +import ( + "context" + "errors" + "fmt" + "net" + "runtime" + "time" + + log "github.com/sirupsen/logrus" +) + +// WGIfaceMonitor monitors the WireGuard interface lifecycle and restarts the engine +// if the interface is deleted externally while the engine is running. +type WGIfaceMonitor struct { + ctx context.Context + cancel context.CancelFunc + done chan struct{} + restartEngine func() +} + +// NewWGIfaceMonitor creates a new WGIfaceMonitor instance. +func NewWGIfaceMonitor(restartEngine func()) *WGIfaceMonitor { + ctx, cancel := context.WithCancel(context.Background()) + return &WGIfaceMonitor{ + ctx: ctx, + cancel: cancel, + done: make(chan struct{}), + restartEngine: restartEngine, + } +} + +// Start begins monitoring the WireGuard interface. +// It relies on the provided context cancellation to stop. +func (m *WGIfaceMonitor) Start(ifaceName string) { + // Skip on mobile platforms as they handle interface lifecycle differently + if runtime.GOOS == "android" || runtime.GOOS == "ios" { + log.Debugf("Interface monitor: skipped on %s platform", runtime.GOOS) + close(m.done) + return + } + + if ifaceName == "" { + log.Debugf("Interface monitor: empty interface name, skipping monitor") + close(m.done) + return + } + + // Get initial interface index to track the specific interface instance + initialIndex, err := getInterfaceIndex(ifaceName) + if err != nil { + log.Debugf("Interface monitor: interface %s not found, skipping monitor", ifaceName) + close(m.done) + return + } + + go func(ctx context.Context, ifaceName string, expectedIndex int) { + defer close(m.done) + log.Infof("Interface monitor: watching %s (index: %d)", ifaceName, expectedIndex) + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + log.Infof("Interface monitor: stopped for %s", ifaceName) + return + case <-ticker.C: + currentIndex, err := getInterfaceIndex(ifaceName) + if err != nil { + // Interface was deleted + log.Infof("Interface monitor: %s deleted, restarting engine", ifaceName) + m.restartEngine() + return + } + + // Check if interface index changed (interface was recreated) + if currentIndex != expectedIndex { + log.Infof("Interface monitor: %s recreated (index changed from %d to %d), restarting engine", + ifaceName, expectedIndex, currentIndex) + m.restartEngine() + return + } + } + } + }(m.ctx, ifaceName, initialIndex) +} + +// Stop stops the monitor and waits for the goroutine to exit. +func (m *WGIfaceMonitor) Stop() { + log.Debugf("Interface monitor: stopping") + m.cancel() + + // Wait for the goroutine to exit with a timeout + select { + case <-m.done: + log.Debugf("Interface monitor: stopped gracefully") + case <-time.After(5 * time.Second): + log.Warnf("Interface monitor: timeout waiting for goroutine to exit") + } +} + +// getInterfaceIndex returns the index of a network interface by name. +// Returns an error if the interface is not found. +func getInterfaceIndex(name string) (int, error) { + if name == "" { + return 0, fmt.Errorf("empty interface name") + } + ifi, err := net.InterfaceByName(name) + if err != nil { + // Check if it's specifically a "not found" error + if errors.Is(err, &net.OpError{}) { + // On some systems, this might be a "not found" error + return 0, fmt.Errorf("interface not found: %w", err) + } + return 0, fmt.Errorf("failed to lookup interface: %w", err) + } + if ifi == nil { + return 0, fmt.Errorf("interface not found") + } + return ifi.Index, nil +} From bf5e12dd8e834b15e24c371659ecc3a27f975c1b Mon Sep 17 00:00:00 2001 From: Hakan Sariman Date: Thu, 18 Sep 2025 19:41:46 +0700 Subject: [PATCH 4/6] Refactor WGIfaceMonitor to use context for lifecycle management and improve engine restart handling --- client/internal/engine.go | 27 +++++---- client/internal/wg_iface_monitor.go | 88 ++++++++++------------------- 2 files changed, 48 insertions(+), 67 deletions(-) diff --git a/client/internal/engine.go b/client/internal/engine.go index 9b62b1a6cd2..be294a2a489 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -196,7 +196,8 @@ type Engine struct { flowManager nftypes.FlowManager // WireGuard interface monitor - wgIfaceMonitor *WGIfaceMonitor + wgIfaceMonitor *WGIfaceMonitor + wgIfaceMonitorWg sync.WaitGroup } // Peer is an instance of the Connection Peer @@ -305,12 +306,6 @@ func (e *Engine) Stop() error { e.srWatcher.Close() } - // Stop WireGuard interface monitor and wait for it to exit - if e.wgIfaceMonitor != nil { - e.wgIfaceMonitor.Stop() - e.wgIfaceMonitor = nil - } - e.statusRecorder.ReplaceOfflinePeers([]peer.State{}) e.statusRecorder.UpdateDNSStates([]peer.NSGroupState{}) e.statusRecorder.UpdateRelayStates([]relay.ProbeResult{}) @@ -346,6 +341,9 @@ func (e *Engine) Stop() error { log.Errorf("failed to persist state: %v", err) } + // Stop WireGuard interface monitor and wait for it to exit + e.wgIfaceMonitorWg.Wait() + return nil } @@ -475,8 +473,18 @@ func (e *Engine) Start() error { e.startNetworkMonitor() // monitor WireGuard interface lifecycle and restart engine on changes - e.wgIfaceMonitor = NewWGIfaceMonitor(e.restartEngine) - e.wgIfaceMonitor.Start(e.wgInterface.Name()) + e.wgIfaceMonitor = NewWGIfaceMonitor() + e.wgIfaceMonitorWg.Add(1) + + go func() { + defer e.wgIfaceMonitorWg.Done() + + if err := e.wgIfaceMonitor.Start(e.ctx, e.wgInterface.Name()); err != nil { + log.Infof("WireGuard interface monitor: %s, restarting engine", err) + e.restartEngine() + } + }() + return nil } @@ -1714,7 +1722,6 @@ func (e *Engine) startNetworkMonitor() { }() } - func (e *Engine) addrViaRoutes(addr netip.Addr) (bool, netip.Prefix, error) { var vpnRoutes []netip.Prefix for _, routes := range e.routeManager.GetClientRoutes() { diff --git a/client/internal/wg_iface_monitor.go b/client/internal/wg_iface_monitor.go index 952b8327bc7..8c4ea834655 100644 --- a/client/internal/wg_iface_monitor.go +++ b/client/internal/wg_iface_monitor.go @@ -14,92 +14,66 @@ import ( // WGIfaceMonitor monitors the WireGuard interface lifecycle and restarts the engine // if the interface is deleted externally while the engine is running. type WGIfaceMonitor struct { - ctx context.Context - cancel context.CancelFunc - done chan struct{} - restartEngine func() + done chan struct{} } // NewWGIfaceMonitor creates a new WGIfaceMonitor instance. -func NewWGIfaceMonitor(restartEngine func()) *WGIfaceMonitor { - ctx, cancel := context.WithCancel(context.Background()) +func NewWGIfaceMonitor() *WGIfaceMonitor { return &WGIfaceMonitor{ - ctx: ctx, - cancel: cancel, - done: make(chan struct{}), - restartEngine: restartEngine, + done: make(chan struct{}), } } // Start begins monitoring the WireGuard interface. // It relies on the provided context cancellation to stop. -func (m *WGIfaceMonitor) Start(ifaceName string) { +func (m *WGIfaceMonitor) Start(ctx context.Context, ifaceName string) error { + defer close(m.done) + // Skip on mobile platforms as they handle interface lifecycle differently if runtime.GOOS == "android" || runtime.GOOS == "ios" { log.Debugf("Interface monitor: skipped on %s platform", runtime.GOOS) - close(m.done) - return + return nil } if ifaceName == "" { log.Debugf("Interface monitor: empty interface name, skipping monitor") - close(m.done) - return + return nil } // Get initial interface index to track the specific interface instance - initialIndex, err := getInterfaceIndex(ifaceName) + expectedIndex, err := getInterfaceIndex(ifaceName) if err != nil { log.Debugf("Interface monitor: interface %s not found, skipping monitor", ifaceName) - close(m.done) - return + return nil } - go func(ctx context.Context, ifaceName string, expectedIndex int) { - defer close(m.done) - log.Infof("Interface monitor: watching %s (index: %d)", ifaceName, expectedIndex) + log.Infof("Interface monitor: watching %s (index: %d)", ifaceName, expectedIndex) - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() - for { - select { - case <-ctx.Done(): - log.Infof("Interface monitor: stopped for %s", ifaceName) - return - case <-ticker.C: - currentIndex, err := getInterfaceIndex(ifaceName) - if err != nil { - // Interface was deleted - log.Infof("Interface monitor: %s deleted, restarting engine", ifaceName) - m.restartEngine() - return - } + for { + select { + case <-ctx.Done(): + log.Infof("Interface monitor: stopped for %s", ifaceName) + return nil + case <-ticker.C: + currentIndex, err := getInterfaceIndex(ifaceName) + if err != nil { + // Interface was deleted + log.Infof("Interface monitor: %s deleted", ifaceName) + return fmt.Errorf("interface %s deleted", ifaceName) + } - // Check if interface index changed (interface was recreated) - if currentIndex != expectedIndex { - log.Infof("Interface monitor: %s recreated (index changed from %d to %d), restarting engine", - ifaceName, expectedIndex, currentIndex) - m.restartEngine() - return - } + // Check if interface index changed (interface was recreated) + if currentIndex != expectedIndex { + log.Infof("Interface monitor: %s recreated (index changed from %d to %d), restarting engine", + ifaceName, expectedIndex, currentIndex) + return fmt.Errorf("interface %s recreated", ifaceName) } } - }(m.ctx, ifaceName, initialIndex) -} - -// Stop stops the monitor and waits for the goroutine to exit. -func (m *WGIfaceMonitor) Stop() { - log.Debugf("Interface monitor: stopping") - m.cancel() - - // Wait for the goroutine to exit with a timeout - select { - case <-m.done: - log.Debugf("Interface monitor: stopped gracefully") - case <-time.After(5 * time.Second): - log.Warnf("Interface monitor: timeout waiting for goroutine to exit") } + } // getInterfaceIndex returns the index of a network interface by name. From 6cc8df9078d217f49baa8865302fa0604f10a98f Mon Sep 17 00:00:00 2001 From: Hakan Sariman Date: Thu, 18 Sep 2025 19:43:15 +0700 Subject: [PATCH 5/6] Clarify error handling in WGIfaceMonitor.Start method documentation --- client/internal/wg_iface_monitor.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/internal/wg_iface_monitor.go b/client/internal/wg_iface_monitor.go index 8c4ea834655..12f97b7cdbe 100644 --- a/client/internal/wg_iface_monitor.go +++ b/client/internal/wg_iface_monitor.go @@ -26,6 +26,8 @@ func NewWGIfaceMonitor() *WGIfaceMonitor { // Start begins monitoring the WireGuard interface. // It relies on the provided context cancellation to stop. +// Start only returns an error if the interface is deleted or recreated, +// context cancellation does not return an error. func (m *WGIfaceMonitor) Start(ctx context.Context, ifaceName string) error { defer close(m.done) From f37cc2756aa5872113f4ab9b8680c07b38d4b3e4 Mon Sep 17 00:00:00 2001 From: Hakan Sariman Date: Thu, 18 Sep 2025 19:52:41 +0700 Subject: [PATCH 6/6] Refactor WGIfaceMonitor.Start to return restart indication and improve error handling --- client/internal/engine.go | 4 +++- client/internal/wg_iface_monitor.go | 16 +++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/client/internal/engine.go b/client/internal/engine.go index be294a2a489..909d6499cbb 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -479,9 +479,11 @@ func (e *Engine) Start() error { go func() { defer e.wgIfaceMonitorWg.Done() - if err := e.wgIfaceMonitor.Start(e.ctx, e.wgInterface.Name()); err != nil { + if shouldRestart, err := e.wgIfaceMonitor.Start(e.ctx, e.wgInterface.Name()); shouldRestart { log.Infof("WireGuard interface monitor: %s, restarting engine", err) e.restartEngine() + } else if err != nil { + log.Warnf("WireGuard interface monitor: %s", err) } }() diff --git a/client/internal/wg_iface_monitor.go b/client/internal/wg_iface_monitor.go index 12f97b7cdbe..78d70c15b1f 100644 --- a/client/internal/wg_iface_monitor.go +++ b/client/internal/wg_iface_monitor.go @@ -26,27 +26,25 @@ func NewWGIfaceMonitor() *WGIfaceMonitor { // Start begins monitoring the WireGuard interface. // It relies on the provided context cancellation to stop. -// Start only returns an error if the interface is deleted or recreated, -// context cancellation does not return an error. -func (m *WGIfaceMonitor) Start(ctx context.Context, ifaceName string) error { +func (m *WGIfaceMonitor) Start(ctx context.Context, ifaceName string) (shouldRestart bool, err error) { defer close(m.done) // Skip on mobile platforms as they handle interface lifecycle differently if runtime.GOOS == "android" || runtime.GOOS == "ios" { log.Debugf("Interface monitor: skipped on %s platform", runtime.GOOS) - return nil + return false, errors.New("not supported on mobile platforms") } if ifaceName == "" { log.Debugf("Interface monitor: empty interface name, skipping monitor") - return nil + return false, errors.New("empty interface name") } // Get initial interface index to track the specific interface instance expectedIndex, err := getInterfaceIndex(ifaceName) if err != nil { log.Debugf("Interface monitor: interface %s not found, skipping monitor", ifaceName) - return nil + return false, fmt.Errorf("interface %s not found: %w", ifaceName, err) } log.Infof("Interface monitor: watching %s (index: %d)", ifaceName, expectedIndex) @@ -58,20 +56,20 @@ func (m *WGIfaceMonitor) Start(ctx context.Context, ifaceName string) error { select { case <-ctx.Done(): log.Infof("Interface monitor: stopped for %s", ifaceName) - return nil + return false, fmt.Errorf("wg interface monitor stopped: %v", ctx.Err()) case <-ticker.C: currentIndex, err := getInterfaceIndex(ifaceName) if err != nil { // Interface was deleted log.Infof("Interface monitor: %s deleted", ifaceName) - return fmt.Errorf("interface %s deleted", ifaceName) + return true, fmt.Errorf("interface %s deleted: %w", ifaceName, err) } // Check if interface index changed (interface was recreated) if currentIndex != expectedIndex { log.Infof("Interface monitor: %s recreated (index changed from %d to %d), restarting engine", ifaceName, expectedIndex, currentIndex) - return fmt.Errorf("interface %s recreated", ifaceName) + return true, nil } } }