mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-18 12:39:54 +00:00
* Stick new watcher creation to actual existence of af the conn and its removal to the removal of such same conn. Avoid debouncing and cross lock dead locking * Discriminate not updated from timeout handshakes * [Recheck watcher ctx cancellation under conn.mu in onWGDisconnected onWGDisconnected only checked conn.ctx (the engine-scoped context), never the watcher's own context. disableWgWatcherIfNeeded cancels the wgWatcherCtx, not conn.ctx, so a disabled watcher's timeout callback did not see the cancellation. handshakeCheck runs lock-free, so between the ctx check in periodicHandshakeCheck and acquiring conn.mu a fast disconnect/reconnect can slip in: the stale watcher then acquires the lock and tears down the *new*, healthy connection based on the old timeout, forcing the guard into an unnecessary reconnect (flap). Recheck watcherCtx.Err() under conn.mu so a superseded watcher exits without touching the connection that replaced it. * Remove verbose comments * Fixup merge conflict leftovers * Fixup context brought by onWGDisconnected
183 lines
5.6 KiB
Go
183 lines
5.6 KiB
Go
package peer
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
|
)
|
|
|
|
const (
|
|
wgHandshakePeriod = 3 * time.Minute
|
|
)
|
|
|
|
var (
|
|
wgHandshakeOvertime = 30 * time.Second // allowed delay in network
|
|
checkPeriod = wgHandshakePeriod + wgHandshakeOvertime
|
|
)
|
|
|
|
type WGInterfaceStater interface {
|
|
GetStats() (map[string]configurer.WGStats, error)
|
|
}
|
|
|
|
// WGWatcher is single-shot: one instance per connection attempt, run once, then discarded.
|
|
// Lifecycle is owned by Conn under conn.mu, so it keeps no "enabled" state to go stale.
|
|
type WGWatcher struct {
|
|
log *log.Entry
|
|
wgIfaceStater WGInterfaceStater
|
|
peerKey string
|
|
stateDump *stateDump
|
|
|
|
// initialHandshake is not thread-safe; never call PrepareInitialHandshake and EnableWgWatcher concurrently.
|
|
initialHandshake time.Time
|
|
|
|
resetCh chan struct{}
|
|
}
|
|
|
|
func NewWGWatcher(log *log.Entry, wgIfaceStater WGInterfaceStater, peerKey string, stateDump *stateDump) *WGWatcher {
|
|
return &WGWatcher{
|
|
log: log,
|
|
wgIfaceStater: wgIfaceStater,
|
|
peerKey: peerKey,
|
|
stateDump: stateDump,
|
|
resetCh: make(chan struct{}, 1),
|
|
}
|
|
}
|
|
|
|
// PrepareInitialHandshake reads the peer's current WireGuard handshake time. It must be
|
|
// called before the peer is (re)configured on the WireGuard interface, so the captured
|
|
// baseline reflects the state prior to this connection attempt instead of racing with
|
|
// that configuration.
|
|
func (w *WGWatcher) PrepareInitialHandshake() {
|
|
w.log.Debugf("enable WireGuard watcher")
|
|
handshake, _ := w.wgState()
|
|
w.initialHandshake = handshake
|
|
}
|
|
|
|
// EnableWgWatcher runs the WireGuard watcher loop using the handshake baseline captured by
|
|
// PrepareInitialHandshake. The watcher runs until ctx is cancelled. Caller is responsible
|
|
// for context lifecycle management. onHandshakeSuccessFn is called only for the first
|
|
// handshake observed by this run, onCheckSuccessFn for every check that observed a fresh
|
|
// handshake, including the first.
|
|
func (w *WGWatcher) EnableWgWatcher(ctx context.Context, enabledTime time.Time, onDisconnectedFn func(), onHandshakeSuccessFn func(when time.Time), onCheckSuccessFn func()) {
|
|
w.periodicHandshakeCheck(ctx, onDisconnectedFn, onHandshakeSuccessFn, onCheckSuccessFn, enabledTime, w.initialHandshake)
|
|
}
|
|
|
|
// Reset signals the watcher that the WireGuard peer has been reset and a new
|
|
// handshake is expected. This restarts the handshake timeout from scratch.
|
|
func (w *WGWatcher) Reset() {
|
|
select {
|
|
case w.resetCh <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// wgStateCheck help to check the state of the WireGuard handshake and relay connection
|
|
func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, onDisconnectedFn func(), onHandshakeSuccessFn func(when time.Time), onCheckSuccessFn func(), enabledTime time.Time, initialHandshake time.Time) {
|
|
w.log.Infof("WireGuard watcher started")
|
|
|
|
timer := time.NewTimer(wgHandshakeOvertime)
|
|
defer timer.Stop()
|
|
|
|
lastHandshake := initialHandshake
|
|
|
|
for {
|
|
select {
|
|
case <-timer.C:
|
|
handshake, ok := w.handshakeCheck(lastHandshake)
|
|
if !ok {
|
|
// early ctx cancel check return
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
onDisconnectedFn()
|
|
return
|
|
}
|
|
if lastHandshake.IsZero() {
|
|
elapsed := calcElapsed(enabledTime, *handshake)
|
|
w.log.Infof("first wg handshake detected within: %.2fsec, (%s)", elapsed, handshake)
|
|
if onHandshakeSuccessFn != nil && ctx.Err() == nil {
|
|
onHandshakeSuccessFn(*handshake)
|
|
}
|
|
}
|
|
|
|
if onCheckSuccessFn != nil && ctx.Err() == nil {
|
|
onCheckSuccessFn()
|
|
}
|
|
|
|
lastHandshake = *handshake
|
|
|
|
resetTime := time.Until(handshake.Add(checkPeriod))
|
|
timer.Reset(resetTime)
|
|
w.stateDump.WGcheckSuccess()
|
|
|
|
w.log.Debugf("WireGuard watcher reset timer: %v", resetTime)
|
|
case <-w.resetCh:
|
|
w.log.Infof("WireGuard watcher received peer reset, restarting handshake timeout")
|
|
lastHandshake = time.Time{}
|
|
enabledTime = time.Now()
|
|
timer.Stop()
|
|
timer.Reset(wgHandshakeOvertime)
|
|
case <-ctx.Done():
|
|
w.log.Infof("WireGuard watcher stopped")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// handshakeCheck checks the WireGuard handshake and return the new handshake time if it is different from the previous one
|
|
func (w *WGWatcher) handshakeCheck(lastHandshake time.Time) (*time.Time, bool) {
|
|
handshake, err := w.wgState()
|
|
if err != nil {
|
|
w.log.Errorf("failed to read wg stats: %v", err)
|
|
return nil, false
|
|
}
|
|
|
|
w.log.Tracef("previous handshake, handshake: %v, %v", lastHandshake, handshake)
|
|
|
|
// the current known handshake did not change
|
|
if handshake.Equal(lastHandshake) {
|
|
w.log.Warnf("WireGuard handshake not updated: %v", handshake)
|
|
return nil, false
|
|
}
|
|
|
|
// in case if the machine is suspended, the handshake time will be in the past
|
|
if handshake.Add(checkPeriod).Before(time.Now()) {
|
|
w.log.Warnf("WireGuard handshake timed out: %v", handshake)
|
|
return nil, false
|
|
}
|
|
|
|
// error handling for handshake time in the future
|
|
if handshake.After(time.Now()) {
|
|
w.log.Warnf("WireGuard handshake is in the future: %v", handshake)
|
|
return nil, false
|
|
}
|
|
|
|
return &handshake, true
|
|
}
|
|
|
|
func (w *WGWatcher) wgState() (time.Time, error) {
|
|
wgStates, err := w.wgIfaceStater.GetStats()
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
wgState, ok := wgStates[w.peerKey]
|
|
if !ok {
|
|
return time.Time{}, fmt.Errorf("peer %s not found in WireGuard endpoints", w.peerKey)
|
|
}
|
|
return wgState.LastHandshake, nil
|
|
}
|
|
|
|
// calcElapsed calculates elapsed time since watcher was enabled.
|
|
// The watcher started after the wg configuration happens, because of this need to normalise the negative value
|
|
func calcElapsed(enabledTime, handshake time.Time) float64 {
|
|
elapsed := handshake.Sub(enabledTime).Seconds()
|
|
if elapsed < 0 {
|
|
elapsed = 0
|
|
}
|
|
return elapsed
|
|
}
|