Skip to content
Merged
Changes from 2 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
84 changes: 84 additions & 0 deletions client/internal/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -1701,6 +1704,87 @@ 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)
Copy link
Preview

Copilot AI Aug 22, 2025

Choose a reason for hiding this comment

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

[nitpick] The polling interval of 2 seconds is a magic number that should be made configurable or defined as a named constant for better maintainability and potential tuning.

Suggested change
ticker := time.NewTicker(2 * time.Second)
ticker := time.NewTicker(wgIfaceMonitorPollInterval)

Copilot uses AI. Check for mistakes.

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{}) {
Copy link
Preview

Copilot AI Aug 22, 2025

Choose a reason for hiding this comment

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

The error check errors.Is(err, &net.OpError{}) is incorrect. errors.Is compares error values, not types. Use var opErr *net.OpError; errors.As(err, &opErr) to check if the error is of type *net.OpError.

Suggested change
if errors.Is(err, &net.OpError{}) {
var opErr *net.OpError
if errors.As(err, &opErr) {

Copilot uses AI. Check for mistakes.

// 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
for _, routes := range e.routeManager.GetClientRoutes() {
Expand Down
Loading