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
165 lines
4.2 KiB
Go
165 lines
4.2 KiB
Go
package peer
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
|
)
|
|
|
|
type MocWgIface struct {
|
|
stop bool
|
|
}
|
|
|
|
func (m *MocWgIface) GetStats() (map[string]configurer.WGStats, error) {
|
|
return map[string]configurer.WGStats{}, nil
|
|
}
|
|
|
|
func (m *MocWgIface) disconnect() {
|
|
m.stop = true
|
|
}
|
|
|
|
type mockHandshakeStats struct {
|
|
mu sync.Mutex
|
|
handshake time.Time
|
|
}
|
|
|
|
func (m *mockHandshakeStats) GetStats() (map[string]configurer.WGStats, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return map[string]configurer.WGStats{"": {LastHandshake: m.handshake}}, nil
|
|
}
|
|
|
|
func (m *mockHandshakeStats) advance() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.handshake = time.Now()
|
|
}
|
|
|
|
// TestWGWatcher_CheckSuccessCallback: onCheckSuccessFn must fire for a fresh
|
|
// handshake even when the watcher started with an existing handshake baseline,
|
|
// the case where onHandshakeSuccessFn stays silent.
|
|
func TestWGWatcher_CheckSuccessCallback(t *testing.T) {
|
|
// checkPeriod bounds how stale a handshake may be before the watcher treats it
|
|
// as a suspended-machine timeout. The first check fires after wgHandshakeOvertime,
|
|
// so keep checkPeriod well above any scheduling jitter to avoid a false timeout
|
|
// converting the expected success into a disconnect on a loaded runner.
|
|
checkPeriod = 1 * time.Minute
|
|
wgHandshakeOvertime = 1 * time.Second
|
|
|
|
mlog := log.WithField("peer", "tet")
|
|
// Use an old baseline so advance() yields a strictly newer handshake even on
|
|
// platforms with coarse clock resolution (Windows), where two time.Now() calls
|
|
// microseconds apart can return the same instant and read as a timed-out handshake.
|
|
stats := &mockHandshakeStats{handshake: time.Now().Add(-time.Hour)}
|
|
watcher := NewWGWatcher(mlog, stats, "", newStateDump("peer", mlog, &Status{}))
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
watcher.PrepareInitialHandshake()
|
|
|
|
firstHandshake := make(chan struct{}, 1)
|
|
checkSuccess := make(chan struct{}, 1)
|
|
go watcher.EnableWgWatcher(ctx, time.Now(), func() {}, func(when time.Time) {
|
|
firstHandshake <- struct{}{}
|
|
}, func() {
|
|
select {
|
|
case checkSuccess <- struct{}{}:
|
|
default:
|
|
}
|
|
})
|
|
|
|
stats.advance()
|
|
|
|
select {
|
|
case <-checkSuccess:
|
|
case <-time.After(10 * time.Second):
|
|
t.Errorf("timeout waiting for check success callback")
|
|
}
|
|
|
|
select {
|
|
case <-firstHandshake:
|
|
t.Errorf("first-handshake callback must not fire for a non-zero baseline")
|
|
default:
|
|
}
|
|
}
|
|
|
|
func TestWGWatcher_EnableWgWatcher(t *testing.T) {
|
|
checkPeriod = 5 * time.Second
|
|
wgHandshakeOvertime = 1 * time.Second
|
|
|
|
mlog := log.WithField("peer", "tet")
|
|
mocWgIface := &MocWgIface{}
|
|
watcher := NewWGWatcher(mlog, mocWgIface, "", newStateDump("peer", mlog, &Status{}))
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
watcher.PrepareInitialHandshake()
|
|
|
|
onDisconnected := make(chan struct{}, 1)
|
|
go watcher.EnableWgWatcher(ctx, time.Now(), func() {
|
|
mlog.Infof("onDisconnectedFn")
|
|
onDisconnected <- struct{}{}
|
|
}, func(when time.Time) {
|
|
mlog.Infof("onHandshakeSuccess: %v", when)
|
|
}, nil)
|
|
|
|
// wait for initial reading
|
|
time.Sleep(2 * time.Second)
|
|
mocWgIface.disconnect()
|
|
|
|
select {
|
|
case <-onDisconnected:
|
|
case <-time.After(10 * time.Second):
|
|
t.Errorf("timeout")
|
|
}
|
|
}
|
|
|
|
func TestWGWatcher_ReEnable(t *testing.T) {
|
|
checkPeriod = 5 * time.Second
|
|
wgHandshakeOvertime = 1 * time.Second
|
|
|
|
mlog := log.WithField("peer", "tet")
|
|
mocWgIface := &MocWgIface{}
|
|
watcher := NewWGWatcher(mlog, mocWgIface, "", newStateDump("peer", mlog, &Status{}))
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
watcher.PrepareInitialHandshake()
|
|
|
|
wg := &sync.WaitGroup{}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
watcher.EnableWgWatcher(ctx, time.Now(), func() {}, func(when time.Time) {}, nil)
|
|
}()
|
|
cancel()
|
|
|
|
wg.Wait()
|
|
|
|
// Re-enable with a new context
|
|
ctx, cancel = context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
watcher.PrepareInitialHandshake()
|
|
|
|
onDisconnected := make(chan struct{}, 1)
|
|
go watcher.EnableWgWatcher(ctx, time.Now(), func() {
|
|
onDisconnected <- struct{}{}
|
|
}, func(when time.Time) {}, nil)
|
|
|
|
time.Sleep(2 * time.Second)
|
|
mocWgIface.disconnect()
|
|
|
|
select {
|
|
case <-onDisconnected:
|
|
case <-time.After(10 * time.Second):
|
|
t.Errorf("timeout")
|
|
}
|
|
}
|