Skip to content

Commit 644ed4b

Browse files
authored
[client] Add WireGuard interface lifecycle monitoring (#4370)
* [client] Add WireGuard interface lifecycle monitoring
1 parent 58faa34 commit 644ed4b

File tree

2 files changed

+121
-0
lines changed

2 files changed

+121
-0
lines changed

client/internal/engine.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ type Engine struct {
198198
latestSyncResponse *mgmProto.SyncResponse
199199
connSemaphore *semaphoregroup.SemaphoreGroup
200200
flowManager nftypes.FlowManager
201+
202+
// WireGuard interface monitor
203+
wgIfaceMonitor *WGIfaceMonitor
204+
wgIfaceMonitorWg sync.WaitGroup
201205
}
202206

203207
// Peer is an instance of the Connection Peer
@@ -341,6 +345,9 @@ func (e *Engine) Stop() error {
341345
log.Errorf("failed to persist state: %v", err)
342346
}
343347

348+
// Stop WireGuard interface monitor and wait for it to exit
349+
e.wgIfaceMonitorWg.Wait()
350+
344351
return nil
345352
}
346353

@@ -479,6 +486,22 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
479486

480487
// starting network monitor at the very last to avoid disruptions
481488
e.startNetworkMonitor()
489+
490+
// monitor WireGuard interface lifecycle and restart engine on changes
491+
e.wgIfaceMonitor = NewWGIfaceMonitor()
492+
e.wgIfaceMonitorWg.Add(1)
493+
494+
go func() {
495+
defer e.wgIfaceMonitorWg.Done()
496+
497+
if shouldRestart, err := e.wgIfaceMonitor.Start(e.ctx, e.wgInterface.Name()); shouldRestart {
498+
log.Infof("WireGuard interface monitor: %s, restarting engine", err)
499+
e.restartEngine()
500+
} else if err != nil {
501+
log.Warnf("WireGuard interface monitor: %s", err)
502+
}
503+
}()
504+
482505
return nil
483506
}
484507

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package internal
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net"
8+
"runtime"
9+
"time"
10+
11+
log "github.com/sirupsen/logrus"
12+
)
13+
14+
// WGIfaceMonitor monitors the WireGuard interface lifecycle and restarts the engine
15+
// if the interface is deleted externally while the engine is running.
16+
type WGIfaceMonitor struct {
17+
done chan struct{}
18+
}
19+
20+
// NewWGIfaceMonitor creates a new WGIfaceMonitor instance.
21+
func NewWGIfaceMonitor() *WGIfaceMonitor {
22+
return &WGIfaceMonitor{
23+
done: make(chan struct{}),
24+
}
25+
}
26+
27+
// Start begins monitoring the WireGuard interface.
28+
// It relies on the provided context cancellation to stop.
29+
func (m *WGIfaceMonitor) Start(ctx context.Context, ifaceName string) (shouldRestart bool, err error) {
30+
defer close(m.done)
31+
32+
// Skip on mobile platforms as they handle interface lifecycle differently
33+
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
34+
log.Debugf("Interface monitor: skipped on %s platform", runtime.GOOS)
35+
return false, errors.New("not supported on mobile platforms")
36+
}
37+
38+
if ifaceName == "" {
39+
log.Debugf("Interface monitor: empty interface name, skipping monitor")
40+
return false, errors.New("empty interface name")
41+
}
42+
43+
// Get initial interface index to track the specific interface instance
44+
expectedIndex, err := getInterfaceIndex(ifaceName)
45+
if err != nil {
46+
log.Debugf("Interface monitor: interface %s not found, skipping monitor", ifaceName)
47+
return false, fmt.Errorf("interface %s not found: %w", ifaceName, err)
48+
}
49+
50+
log.Infof("Interface monitor: watching %s (index: %d)", ifaceName, expectedIndex)
51+
52+
ticker := time.NewTicker(2 * time.Second)
53+
defer ticker.Stop()
54+
55+
for {
56+
select {
57+
case <-ctx.Done():
58+
log.Infof("Interface monitor: stopped for %s", ifaceName)
59+
return false, fmt.Errorf("wg interface monitor stopped: %v", ctx.Err())
60+
case <-ticker.C:
61+
currentIndex, err := getInterfaceIndex(ifaceName)
62+
if err != nil {
63+
// Interface was deleted
64+
log.Infof("Interface monitor: %s deleted", ifaceName)
65+
return true, fmt.Errorf("interface %s deleted: %w", ifaceName, err)
66+
}
67+
68+
// Check if interface index changed (interface was recreated)
69+
if currentIndex != expectedIndex {
70+
log.Infof("Interface monitor: %s recreated (index changed from %d to %d), restarting engine",
71+
ifaceName, expectedIndex, currentIndex)
72+
return true, nil
73+
}
74+
}
75+
}
76+
77+
}
78+
79+
// getInterfaceIndex returns the index of a network interface by name.
80+
// Returns an error if the interface is not found.
81+
func getInterfaceIndex(name string) (int, error) {
82+
if name == "" {
83+
return 0, fmt.Errorf("empty interface name")
84+
}
85+
ifi, err := net.InterfaceByName(name)
86+
if err != nil {
87+
// Check if it's specifically a "not found" error
88+
if errors.Is(err, &net.OpError{}) {
89+
// On some systems, this might be a "not found" error
90+
return 0, fmt.Errorf("interface not found: %w", err)
91+
}
92+
return 0, fmt.Errorf("failed to lookup interface: %w", err)
93+
}
94+
if ifi == nil {
95+
return 0, fmt.Errorf("interface not found")
96+
}
97+
return ifi.Index, nil
98+
}

0 commit comments

Comments
 (0)