Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions client/internal/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ type Engine struct {
latestSyncResponse *mgmProto.SyncResponse
connSemaphore *semaphoregroup.SemaphoreGroup
flowManager nftypes.FlowManager

// WireGuard interface monitor
wgIfaceMonitor *WGIfaceMonitor
wgIfaceMonitorWg sync.WaitGroup
}

// Peer is an instance of the Connection Peer
Expand Down Expand Up @@ -337,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
}

Expand Down Expand Up @@ -464,6 +471,22 @@ 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.wgIfaceMonitor = NewWGIfaceMonitor()
e.wgIfaceMonitorWg.Add(1)

go func() {
defer e.wgIfaceMonitorWg.Done()

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)
}
}()

return nil
}

Expand Down
98 changes: 98 additions & 0 deletions client/internal/wg_iface_monitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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 {
done chan struct{}
}

// NewWGIfaceMonitor creates a new WGIfaceMonitor instance.
func NewWGIfaceMonitor() *WGIfaceMonitor {
return &WGIfaceMonitor{
done: make(chan struct{}),
}
}

// Start begins monitoring the WireGuard interface.
// It relies on the provided context cancellation to stop.
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" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably put this check in the code calling NewWGIfaceMonitor() instead of here, it's not the Monitor's concern whether it should run or not, especially when you're already passing an unspecified callback function.

log.Debugf("Interface monitor: skipped on %s platform", runtime.GOOS)
return false, errors.New("not supported on mobile platforms")
}

if ifaceName == "" {
log.Debugf("Interface monitor: empty interface name, skipping monitor")
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 false, fmt.Errorf("interface %s not found: %w", ifaceName, err)
}

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 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 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 true, nil
}
}
}

}

// 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
}
Loading