mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-23 13:59:09 +02:00
252 lines
10 KiB
Go
252 lines
10 KiB
Go
package configurer
|
|
|
|
import (
|
|
"net"
|
|
"net/netip"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
|
|
"github.com/netbirdio/netbird/client/iface/bind"
|
|
"github.com/netbirdio/netbird/client/iface/wgbackend"
|
|
)
|
|
|
|
// newTestUSPConfigurer builds a configurer over a real wireguard-go device backed by an
|
|
// in-memory TUN. The device stays down, so no socket is opened and no privileges are needed.
|
|
func newTestUSPConfigurer(t *testing.T) *WGUSPConfigurer {
|
|
t.Helper()
|
|
|
|
dev := wgbackend.NewDevice(newIdleTUN(), wgbackend.NewDefaultBind(), wgbackend.NewLogger(wgbackend.LogLevelSilent, ""))
|
|
t.Cleanup(dev.Close)
|
|
|
|
c := NewUSPConfigurerNoUAPI(dev, "wgtest0", bind.NewActivityRecorder())
|
|
|
|
key, err := wgtypes.GeneratePrivateKey()
|
|
require.NoError(t, err, "generate device private key")
|
|
require.NoError(t, c.ConfigureInterface(key.String(), 0), "configure test device")
|
|
|
|
return c
|
|
}
|
|
|
|
// seedPeers adds count peers, each with a /32 overlay address, and returns their public keys.
|
|
func seedPeers(t *testing.T, c *WGUSPConfigurer, count int) []string {
|
|
t.Helper()
|
|
|
|
keys := make([]string, 0, count)
|
|
for i := 0; i < count; i++ {
|
|
priv, err := wgtypes.GeneratePrivateKey()
|
|
require.NoError(t, err, "generate peer private key")
|
|
pub := priv.PublicKey().String()
|
|
|
|
addr := netip.PrefixFrom(netip.AddrFrom4([4]byte{100, 64, byte(i >> 8), byte(i)}), 32)
|
|
require.NoError(t, c.UpdatePeer(pub, []netip.Prefix{addr}, 25*time.Second, nil, nil), "add peer")
|
|
keys = append(keys, pub)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func peerAllowedIPs(t *testing.T, c *WGUSPConfigurer, peerKey string) []string {
|
|
t.Helper()
|
|
|
|
stats, err := c.FullStats()
|
|
require.NoError(t, err, "read device stats")
|
|
|
|
for _, p := range stats.Peers {
|
|
if p.PublicKey != peerKey {
|
|
continue
|
|
}
|
|
got := make([]string, 0, len(p.AllowedIPs))
|
|
for _, ipNet := range p.AllowedIPs {
|
|
got = append(got, ipNet.String())
|
|
}
|
|
return got
|
|
}
|
|
t.Fatalf("peer %s not found on device", peerKey)
|
|
return nil
|
|
}
|
|
|
|
// TestRemoveEndpointAddressPreservesRoutedAllowedIPs covers the prefixes the route manager
|
|
// attaches to a routing peer through AddAllowedIP. Those are not known to the peer.Conn that
|
|
// triggers the endpoint removal, so dropping them here would silently blackhole every route
|
|
// behind that peer on each relay or ICE disconnect.
|
|
func TestRemoveEndpointAddressPreservesRoutedAllowedIPs(t *testing.T) {
|
|
c := newTestUSPConfigurer(t)
|
|
peerKey := seedPeers(t, c, 3)[1]
|
|
|
|
routed := []netip.Prefix{
|
|
netip.MustParsePrefix("10.20.0.0/16"),
|
|
netip.MustParsePrefix("192.168.7.0/24"),
|
|
}
|
|
for _, prefix := range routed {
|
|
require.NoError(t, c.AddAllowedIP(peerKey, prefix), "add routed prefix")
|
|
}
|
|
|
|
before := peerAllowedIPs(t, c, peerKey)
|
|
require.Len(t, before, 3, "peer should hold its overlay address plus both routed prefixes")
|
|
|
|
require.NoError(t, c.RemoveEndpointAddress(peerKey), "remove endpoint address")
|
|
|
|
assert.ElementsMatch(t, before, peerAllowedIPs(t, c, peerKey),
|
|
"allowed IPs must survive the endpoint removal unchanged")
|
|
}
|
|
|
|
// TestRemoveEndpointAddressDoesNotScaleWithPeerCount is the regression guard for the actual
|
|
// defect: clearing one peer's endpoint used to dump and parse the whole device, so its cost
|
|
// grew with the size of the network map. On a routing peer with thousands of peers that dump
|
|
// runs on every relay and ICE transition, under the interface lock.
|
|
func TestRemoveEndpointAddressDoesNotScaleWithPeerCount(t *testing.T) {
|
|
measure := func(peerCount int) float64 {
|
|
c := newTestUSPConfigurer(t)
|
|
peerKey := seedPeers(t, c, peerCount)[peerCount/2]
|
|
|
|
return testing.AllocsPerRun(5, func() {
|
|
require.NoError(t, c.RemoveEndpointAddress(peerKey), "remove endpoint address")
|
|
})
|
|
}
|
|
|
|
small := measure(64)
|
|
large := measure(1024)
|
|
|
|
assert.Less(t, large, small*2,
|
|
"clearing one endpoint allocated %.0f objects with 1024 peers against %.0f with 64: the cost still scales with the peer count",
|
|
large, small)
|
|
}
|
|
|
|
func TestRemoveAllowedIPKeepsTheOtherPrefixes(t *testing.T) {
|
|
c := newTestUSPConfigurer(t)
|
|
peerKey := seedPeers(t, c, 3)[0]
|
|
routed := netip.MustParsePrefix("10.20.0.0/16")
|
|
require.NoError(t, c.AddAllowedIP(peerKey, routed), "add routed prefix")
|
|
require.NoError(t, c.AddAllowedIP(peerKey, netip.MustParsePrefix("192.168.7.0/24")), "add routed prefix")
|
|
|
|
require.NoError(t, c.RemoveAllowedIP(peerKey, routed), "remove routed prefix")
|
|
|
|
assert.ElementsMatch(t, []string{"100.64.0.0/32", "192.168.7.0/24"}, peerAllowedIPs(t, c, peerKey),
|
|
"only the removed prefix should be gone")
|
|
|
|
assert.ErrorIs(t, c.RemoveAllowedIP(peerKey, routed), ErrAllowedIPNotFound,
|
|
"removing a prefix that is no longer configured must be reported")
|
|
}
|
|
|
|
// TestAddAllowedIPOnAbsentPeerDoesNotResurrectIt covers the lazy connection window documented
|
|
// in #6863: AddAllowedIP is update-only, a silent no-op when the peer is absent, and clearing
|
|
// the endpoint of an absent peer must not create one either.
|
|
func TestAddAllowedIPOnAbsentPeerDoesNotResurrectIt(t *testing.T) {
|
|
c := newTestUSPConfigurer(t)
|
|
seedPeers(t, c, 2)
|
|
|
|
priv, err := wgtypes.GeneratePrivateKey()
|
|
require.NoError(t, err, "generate peer private key")
|
|
absent := priv.PublicKey().String()
|
|
|
|
require.NoError(t, c.AddAllowedIP(absent, netip.MustParsePrefix("10.20.0.0/16")),
|
|
"update-only add on an absent peer is a silent no-op")
|
|
|
|
stats, err := c.FullStats()
|
|
require.NoError(t, err, "read device stats")
|
|
require.Len(t, stats.Peers, 2, "the absent peer must not have been created by AddAllowedIP")
|
|
|
|
assert.ErrorIs(t, c.RemoveEndpointAddress(absent), ErrPeerNotFound,
|
|
"clearing the endpoint of a peer the device does not have must fail")
|
|
|
|
stats, err = c.FullStats()
|
|
require.NoError(t, err, "read device stats")
|
|
assert.Len(t, stats.Peers, 2, "no peer may be created while clearing an endpoint")
|
|
}
|
|
|
|
// TestRemoveEndpointAddressDoesNotStealAPrefixFromAnotherPeer covers WireGuard's rule that an
|
|
// allowed IP belongs to exactly one peer: configuring a prefix on a peer takes it away from
|
|
// whichever peer held it before. UpdatePeer relies on that rule rather than removing the prefix
|
|
// from the previous holder itself, so a prefix handed over between peers must not come back.
|
|
func TestRemoveEndpointAddressDoesNotStealAPrefixFromAnotherPeer(t *testing.T) {
|
|
c := newTestUSPConfigurer(t)
|
|
keys := seedPeers(t, c, 2)
|
|
peerA, peerB := keys[0], keys[1]
|
|
routed := netip.MustParsePrefix("10.20.0.0/16")
|
|
|
|
require.NoError(t, c.AddAllowedIP(peerA, routed), "give the prefix to A")
|
|
require.Contains(t, peerAllowedIPs(t, c, peerA), routed.String(), "A must hold the prefix")
|
|
|
|
// The route moves to B. The device takes it away from A on its own.
|
|
require.NoError(t, c.AddAllowedIP(peerB, routed), "hand the prefix over to B")
|
|
require.Contains(t, peerAllowedIPs(t, c, peerB), routed.String(), "B must hold the prefix")
|
|
require.NotContains(t, peerAllowedIPs(t, c, peerA), routed.String(), "the device must have taken it from A")
|
|
|
|
require.NoError(t, c.RemoveEndpointAddress(peerA), "clear A's endpoint")
|
|
|
|
assert.NotContains(t, peerAllowedIPs(t, c, peerA), routed.String(),
|
|
"clearing A's endpoint must not take the prefix back from B")
|
|
assert.Contains(t, peerAllowedIPs(t, c, peerB), routed.String(),
|
|
"B must still hold the prefix")
|
|
}
|
|
|
|
// TestPresharedKeyCreatedPeerTakesPartInPrefixHandover covers a peer created by a preshared
|
|
// key write rather than by a peer update. Rosenpass applies a peer's first key without
|
|
// updateOnly, which creates the peer on the device, so a store that ignored that operation
|
|
// would treat the peer as unknown and would not account for a prefix later handed over to it.
|
|
func TestPresharedKeyCreatedPeerTakesPartInPrefixHandover(t *testing.T) {
|
|
c := newTestUSPConfigurer(t)
|
|
peerA := seedPeers(t, c, 1)[0]
|
|
routed := netip.MustParsePrefix("10.20.0.0/16")
|
|
require.NoError(t, c.AddAllowedIP(peerA, routed), "give the prefix to A")
|
|
|
|
priv, err := wgtypes.GeneratePrivateKey()
|
|
require.NoError(t, err, "generate peer private key")
|
|
peerB := priv.PublicKey().String()
|
|
|
|
psk, err := wgtypes.GenerateKey()
|
|
require.NoError(t, err, "generate preshared key")
|
|
require.NoError(t, c.SetPresharedKey(peerB, psk, false), "a first key creates the peer")
|
|
|
|
require.NoError(t, c.AddAllowedIP(peerB, routed), "hand the prefix over to B")
|
|
require.Contains(t, peerAllowedIPs(t, c, peerB), routed.String(), "B must hold the prefix")
|
|
|
|
require.NoError(t, c.RemoveEndpointAddress(peerA), "clear A's endpoint")
|
|
|
|
assert.NotContains(t, peerAllowedIPs(t, c, peerA), routed.String(),
|
|
"clearing A's endpoint must not take the prefix back from B")
|
|
assert.Contains(t, peerAllowedIPs(t, c, peerB), routed.String(), "B must still hold the prefix")
|
|
}
|
|
|
|
// TestUpdatePeerDoesNotWidenAMappedPrefixOnTheDevice is the end to end form of the
|
|
// conversion: a v4-mapped prefix must not reach the device as a zero length allowed IP,
|
|
// which would route every v4 address to that peer.
|
|
func TestUpdatePeerDoesNotWidenAMappedPrefixOnTheDevice(t *testing.T) {
|
|
c := newTestUSPConfigurer(t)
|
|
|
|
priv, err := wgtypes.GeneratePrivateKey()
|
|
require.NoError(t, err, "generate peer private key")
|
|
peerKey := priv.PublicKey().String()
|
|
|
|
mapped := netip.MustParsePrefix("::ffff:10.1.2.3/112")
|
|
require.NoError(t, c.UpdatePeer(peerKey, []netip.Prefix{mapped}, 25*time.Second, nil, nil), "add peer")
|
|
|
|
onDevice := peerAllowedIPs(t, c, peerKey)
|
|
assert.NotContains(t, onDevice, "0.0.0.0/0", "the device must not be given a catch-all allowed IP")
|
|
assert.Equal(t, []string{"10.1.0.0/16"}, onDevice, "the device holds the normalized prefix")
|
|
}
|
|
|
|
// TestUpdatePeerWithAnUnusableEndpointTouchesNothing pins the ordering: the endpoint is
|
|
// parsed before the device is configured, so a failure cannot leave a half-configured peer
|
|
// on the device.
|
|
func TestUpdatePeerWithAnUnusableEndpointTouchesNothing(t *testing.T) {
|
|
c := newTestUSPConfigurer(t)
|
|
seedPeers(t, c, 2)
|
|
|
|
priv, err := wgtypes.GeneratePrivateKey()
|
|
require.NoError(t, err, "generate peer private key")
|
|
peerKey := priv.PublicKey().String()
|
|
|
|
// A three byte address has no textual form netip can parse back.
|
|
endpoint := &net.UDPAddr{IP: net.IP{1, 2, 3}, Port: 51820}
|
|
require.Error(t, c.UpdatePeer(peerKey, []netip.Prefix{netip.MustParsePrefix("10.30.0.0/16")},
|
|
25*time.Second, endpoint, nil), "an unusable endpoint must fail the update")
|
|
|
|
stats, err := c.FullStats()
|
|
require.NoError(t, err, "read device stats")
|
|
assert.Len(t, stats.Peers, 2, "the peer must not have reached the device")
|
|
}
|