mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-22 21:39:08 +02:00
The recovery test could time out its own bootstraps under CI load, the non-capable test passed with the cancellation removed, and the decoder rejection test only exercised the encoder. The timing spike ran on every go test; it is a benchmark now.
91 lines
3.4 KiB
Go
91 lines
3.4 KiB
Go
package pqkem
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestConcurrency_RecoversViaResignalAfterDataPathBreak exercises the A-light recovery:
|
|
// a data-path rotation can no longer converge (OnRekeyFailed), and re-bootstrapping over
|
|
// signalling resyncs both peers on a fresh PSK — even while the data path stays broken,
|
|
// since the signal channel is independent of it.
|
|
func TestConcurrency_RecoversViaResignalAfterDataPathBreak(t *testing.T) {
|
|
dA, dB, wgA, wgB, lbB := pair(t)
|
|
defer dA.Stop()
|
|
defer dB.Stop()
|
|
|
|
// A fast retransmit tick, set before any exchange loop spawns because the loop reads it
|
|
// unguarded at start. The retry budget stays generous for the two bootstraps, which
|
|
// must succeed: with a tight budget the loop could time an exchange out while the test
|
|
// goroutine is descheduled between the offer and the answer.
|
|
dB.retryInterval = 5 * time.Millisecond
|
|
setRetryBudget(dB, 1000, 1)
|
|
|
|
bootstrap(t, dA, dB)
|
|
dA.OnDataPathRekeyed("bbbb", 0)
|
|
dB.OnDataPathRekeyed("aaaa", 0)
|
|
psk1 := wgB.psk("aaaa")
|
|
require.NotEqual(t, PSK{}, psk1)
|
|
require.Equal(t, psk1, wgA.psk("bbbb"), "converged on the same PSK after bootstrap+rotation")
|
|
|
|
// Data path breaks: the rotation can no longer converge, and with a budget of two
|
|
// retransmits a single miss raises OnRekeyFailed.
|
|
setRetryBudget(dB, 2, 1)
|
|
lbB.drop.Store(true)
|
|
_, err := dB.startExchangeTest("aaaa", false, ExchangeID{})
|
|
require.NoError(t, err)
|
|
require.Eventually(t, func() bool { return failedCount(wgB) >= 1 }, time.Second, 5*time.Millisecond)
|
|
|
|
// Recovery: re-bootstrap over signalling with the data path STILL broken. It must
|
|
// still converge (signal is independent of the data path) on a fresh PSK.
|
|
setRetryBudget(dB, 1000, 1)
|
|
bootstrap(t, dA, dB)
|
|
psk2 := wgB.psk("aaaa")
|
|
require.NotEqual(t, psk1, psk2, "recovery derived a fresh PSK")
|
|
require.Equal(t, psk2, wgA.psk("bbbb"), "both sides resync after recovery")
|
|
}
|
|
|
|
// setRetryBudget changes the retry and failure budgets of a running manager. The exchange
|
|
// loops read both under the manager lock, so they can change between phases of a test.
|
|
func setRetryBudget(m *Manager, maxRetries, maxRekeyFailures int) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.maxRetries = maxRetries
|
|
m.maxRekeyFailures = maxRekeyFailures
|
|
}
|
|
|
|
// TestConcurrency_ConcurrentRekeysNoRace hammers both managers with concurrent rotation
|
|
// clocks from many goroutines. Its primary job (with -race) is to prove the single-lock
|
|
// state machine has no data races or deadlocks under contention; a final deterministic
|
|
// bootstrap then asserts there is no split-brain (both sides on the same PSK).
|
|
func TestConcurrency_ConcurrentRekeysNoRace(t *testing.T) {
|
|
dA, dB, wgA, wgB, _ := pair(t)
|
|
defer dA.Stop()
|
|
defer dB.Stop()
|
|
|
|
bootstrap(t, dA, dB)
|
|
|
|
var wg sync.WaitGroup
|
|
for g := 0; g < 8; g++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 50; i++ {
|
|
dB.OnDataPathRekeyed("aaaa", 0) // initiator chains a rotation
|
|
dA.OnDataPathRekeyed("bbbb", 0) // responder side is a no-op, still stresses the lock
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
// The storm may leave an exchange mid-flight (concurrent cancellation). Force a
|
|
// clean convergence over signalling, then assert no split-brain.
|
|
bootstrap(t, dA, dB)
|
|
a, b := wgA.psk("bbbb"), wgB.psk("aaaa")
|
|
require.NotEqual(t, PSK{}, b)
|
|
require.Equal(t, a, b, "both sides converge on the same PSK, no split-brain")
|
|
}
|