mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-29 11:01:29 +02:00
peer -> peerAddrs have types for remoteID and localID t.Close log error Manager SetTransport -> Start
170 lines
4.2 KiB
Go
170 lines
4.2 KiB
Go
package pqkem
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// netSwitch is an in-memory UDP fabric: transports register their endpoint and get
|
|
// datagrams delivered to their inbound handler.
|
|
type netSwitch struct {
|
|
mu sync.Mutex
|
|
h map[netip.AddrPort]func(netip.AddrPort, []byte)
|
|
}
|
|
|
|
func newSwitch() *netSwitch {
|
|
return &netSwitch{h: map[netip.AddrPort]func(netip.AddrPort, []byte){}}
|
|
}
|
|
|
|
func (s *netSwitch) register(ep netip.AddrPort, fn func(netip.AddrPort, []byte)) {
|
|
s.mu.Lock()
|
|
s.h[ep] = fn
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *netSwitch) deliver(dst, src netip.AddrPort, msg []byte) error {
|
|
s.mu.Lock()
|
|
fn := s.h[dst]
|
|
s.mu.Unlock()
|
|
if fn == nil {
|
|
return fmt.Errorf("no route to %s", dst)
|
|
}
|
|
fn(src, msg)
|
|
return nil
|
|
}
|
|
|
|
// loopback is an endpoint-based pqkem.Transport over a netSwitch, with a switchable
|
|
// drop flag.
|
|
type loopback struct {
|
|
ep netip.AddrPort
|
|
sw *netSwitch
|
|
drop atomic.Bool
|
|
}
|
|
|
|
func (l *loopback) Send(dst netip.AddrPort, msg []byte) error {
|
|
if l.drop.Load() {
|
|
return nil
|
|
}
|
|
return l.sw.deliver(dst, l.ep, append([]byte(nil), msg...))
|
|
}
|
|
|
|
func (l *loopback) LocalPort() int { return int(l.ep.Port()) }
|
|
func (l *loopback) Run(onInbound func(netip.AddrPort, []byte)) { l.sw.register(l.ep, onInbound) }
|
|
func (l *loopback) Close() error { return nil }
|
|
|
|
type fakeWG struct {
|
|
mu sync.Mutex
|
|
psks map[RemoteID]PSK
|
|
failed []RemoteID
|
|
}
|
|
|
|
func newFakeWG() *fakeWG { return &fakeWG{psks: map[RemoteID]PSK{}} }
|
|
|
|
func (f *fakeWG) OnNewPSKReady(remoteID RemoteID, psk PSK) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.psks[remoteID] = psk
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeWG) OnRekeyFailed(remoteID RemoteID) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.failed = append(f.failed, remoteID)
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeWG) psk(peer RemoteID) PSK {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.psks[peer]
|
|
}
|
|
|
|
var (
|
|
epA = netip.MustParseAddrPort("100.64.0.1:51833")
|
|
epB = netip.MustParseAddrPort("100.64.0.2:51833")
|
|
)
|
|
|
|
// pair builds two wired managers (B is the initiator, "bbbb" > "aaaa") sharing a
|
|
// netSwitch, with each peer's data-path endpoint registered. lbB is B's loopback
|
|
// (for toggling drop).
|
|
func pair(t *testing.T) (dA, dB *Manager, wgA, wgB *fakeWG, lbB *loopback) {
|
|
t.Helper()
|
|
sw := newSwitch()
|
|
wgA = newFakeWG()
|
|
wgB = newFakeWG()
|
|
dA = NewManager("aaaa", wgA, nil)
|
|
dB = NewManager("bbbb", wgB, nil)
|
|
dA.Start(&loopback{ep: epA, sw: sw})
|
|
lbB = &loopback{ep: epB, sw: sw}
|
|
dB.Start(lbB)
|
|
dA.AddPeer("bbbb", epB)
|
|
dB.AddPeer("aaaa", epA)
|
|
return dA, dB, wgA, wgB, lbB
|
|
}
|
|
|
|
// 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: B (initiator) chains the next offer 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", 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", newFakeWG(), nil)
|
|
dA.Start(&loopback{ep: epA, sw: newSwitch()})
|
|
dA.Stop()
|
|
dA.Stop() // must not panic or hang
|
|
}
|