Files
netbird/client/internal/pqkem/manager_test.go
T
riccardom 0aeed6ae5b Removes confirm. Uses next offer to deliver confirmation/ack of previous round
We clock the next Offer initiation to the OnDataPathRekeyed, so we have 2 minutes
ahead of us to do our attempts and stuff before to give up.
On failure, we will know because we will not receive a new answer.. but more importantly
the wg handshake will fail :D
2026-09-11 14:48:54 +02:00

123 lines
3.1 KiB
Go

package pqkem
import (
"sync"
"testing"
"github.com/stretchr/testify/require"
)
// loopback is a data-path transport: SendDataPath delivers synchronously to the peer
// manager's OnDataPathMessage, attributing it to localID (the sender). The signalling
// channel is driven by the test directly via the SignalX methods.
type loopback struct {
localID string
peer *Manager
}
func (l *loopback) SendDataPath(remoteID string, msg []byte) error {
cp := append([]byte(nil), msg...)
return l.peer.OnDataPathMessage(l.localID, cp)
}
type fakeWG struct {
mu sync.Mutex
psks map[string]PSK
failed []string
}
func newFakeWG() *fakeWG { return &fakeWG{psks: map[string]PSK{}} }
func (f *fakeWG) OnNewPSKReady(remoteID string, psk PSK) error {
f.mu.Lock()
defer f.mu.Unlock()
f.psks[remoteID] = psk
return nil
}
func (f *fakeWG) OnRekeyFailed(remoteID string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.failed = append(f.failed, remoteID)
return nil
}
func (f *fakeWG) psk(peer string) PSK {
f.mu.Lock()
defer f.mu.Unlock()
return f.psks[peer]
}
// pair builds two wired managers (B is the initiator, "bbbb" > "aaaa").
func pair(t *testing.T) (dA, dB *Manager, wgA, wgB *fakeWG) {
t.Helper()
lbA := &loopback{localID: "aaaa"}
lbB := &loopback{localID: "bbbb"}
wgA = newFakeWG()
wgB = newFakeWG()
dA = NewManager("aaaa", lbA, wgA, nil)
dB = NewManager("bbbb", lbB, wgB, nil)
lbA.peer = dB
lbB.peer = dA
return dA, dB, wgA, wgB
}
// bootstrap runs the signalling offer/answer (the test plays the host carrying bytes).
func bootstrap(t *testing.T, dA, dB *Manager) {
t.Helper()
offer, err := dB.SignalOffer("aaaa")
require.NoError(t, err)
require.NotNil(t, offer)
answer, err := dA.SignalOnOffer("bbbb", offer)
require.NoError(t, err)
require.NotNil(t, answer)
require.NoError(t, dB.SignalOnAnswer("aaaa", answer))
}
func TestManager_BootstrapDerivesSamePSK(t *testing.T) {
dA, dB, wgA, wgB := pair(t)
defer dA.Stop()
defer dB.Stop()
bootstrap(t, dA, dB)
pskA := wgA.psk("bbbb")
pskB := wgB.psk("aaaa")
require.NotEqual(t, PSK{}, pskA)
require.Equal(t, pskB, pskA, "both sides derive the same PSK from the bootstrap exchange")
}
func TestManager_ChainRotatesAndAcks(t *testing.T) {
dA, dB, wgA, wgB := pair(t)
defer dA.Stop()
defer dB.Stop()
bootstrap(t, dA, dB)
psk1 := wgB.psk("aaaa")
// Data path up on both sides; B chains the next offer (acking exchange 1) over the
// data path, which rotates both to a fresh PSK and acknowledges A.
dA.OnDataPathRekeyed("bbbb")
dB.OnDataPathRekeyed("aaaa")
psk2A := wgA.psk("bbbb")
psk2B := wgB.psk("aaaa")
require.Equal(t, psk2B, psk2A, "both sides converge on the rotated PSK")
require.NotEqual(t, psk1, psk2B, "the chain rotated to a new PSK")
}
func TestManager_NonInitiatorReturnsNoOffer(t *testing.T) {
dA := NewManager("aaaa", &loopback{localID: "aaaa"}, newFakeWG(), nil)
defer dA.Stop()
offer, err := dA.SignalOffer("bbbb") // not the initiator vs "bbbb"
require.NoError(t, err)
require.Nil(t, offer)
}
func TestManager_StopIsIdempotent(t *testing.T) {
dA := NewManager("aaaa", &loopback{localID: "aaaa"}, newFakeWG(), nil)
dA.Stop()
dA.Stop() // must not panic or hang
}