Files
netbird/client/iface/configurer/usp_test.go
T

224 lines
8.2 KiB
Go

package configurer
import (
"encoding/hex"
"net"
"net/netip"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/iface/wgbackend"
)
// idleTUN never reports an up event, so the wireguard-go device stays down: no socket is
// opened, no peer goroutine starts, and the configuration state is all the tests observe.
type idleTUN struct {
events chan wgbackend.TUNEvent
closed chan struct{}
}
func newIdleTUN() *idleTUN {
return &idleTUN{events: make(chan wgbackend.TUNEvent), closed: make(chan struct{})}
}
func (t *idleTUN) File() *os.File { return nil }
func (t *idleTUN) Read([][]byte, []int, int) (int, error) {
<-t.closed
return 0, os.ErrClosed
}
func (t *idleTUN) Write(bufs [][]byte, _ int) (int, error) { return len(bufs), nil }
func (t *idleTUN) MTU() (int, error) { return 1420, nil }
func (t *idleTUN) Name() (string, error) { return "idle", nil }
func (t *idleTUN) Events() <-chan wgbackend.TUNEvent { return t.events }
func (t *idleTUN) Close() error {
close(t.closed)
close(t.events)
return nil
}
func (t *idleTUN) BatchSize() int { return 1 }
func TestConfigureInterfaceAppliesKeyAndPortAndReplacesPeers(t *testing.T) {
c := newTestUSPConfigurer(t)
seedPeers(t, c, 2)
key, err := wgtypes.GeneratePrivateKey()
require.NoError(t, err, "generate device private key")
require.NoError(t, c.ConfigureInterface(key.String(), 51820), "reconfigure the interface")
stats, err := c.FullStats()
require.NoError(t, err, "read device stats")
assert.Equal(t, key.PublicKey().String(), stats.PublicKey, "device public key")
assert.Equal(t, 51820, stats.ListenPort, "listen port")
assert.Empty(t, stats.Peers, "configuring the interface replaces every peer")
}
func TestConfigureInterfaceRejectsAnInvalidPort(t *testing.T) {
c := newTestUSPConfigurer(t)
key, err := wgtypes.GeneratePrivateKey()
require.NoError(t, err, "generate device private key")
assert.Error(t, c.ConfigureInterface(key.String(), 70000), "a port above 65535 must be rejected")
}
func TestGetStatsIsKeyedByPeerKey(t *testing.T) {
c := newTestUSPConfigurer(t)
keys := seedPeers(t, c, 3)
stats, err := c.GetStats()
require.NoError(t, err, "read stats")
require.Len(t, stats, 3, "one entry per peer")
for _, key := range keys {
entry, ok := stats[key]
require.True(t, ok, "peer %s must be reported", key)
assert.Zero(t, entry.TxBytes, "no traffic was sent")
assert.Zero(t, entry.RxBytes, "no traffic was received")
assert.True(t, entry.LastHandshake.IsZero(), "no handshake happened")
}
}
func TestFullStatsMatchesTheDeviceDump(t *testing.T) {
c := newTestUSPConfigurer(t)
priv, err := wgtypes.GeneratePrivateKey()
require.NoError(t, err, "generate peer private key")
peerKey := priv.PublicKey().String()
psk, err := wgtypes.GenerateKey()
require.NoError(t, err, "generate preshared key")
endpoint := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 10), Port: 51820}
prefixes := []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32"), netip.MustParsePrefix("10.20.0.0/16")}
require.NoError(t, c.UpdatePeer(peerKey, prefixes, 25*time.Second, endpoint, &psk), "add peer")
stats, err := c.FullStats()
require.NoError(t, err, "read device stats")
require.Len(t, stats.Peers, 1, "one peer on the device")
peer := stats.Peers[0]
assert.Equal(t, peerKey, peer.PublicKey, "peer key")
assert.Equal(t, "192.0.2.10:51820", peer.Endpoint.String(), "endpoint")
assert.Equal(t, [32]byte(psk), peer.PresharedKey, "preshared key")
assert.Equal(t, []string{"100.64.0.1/32", "10.20.0.0/16"}, ipNetStrings(peer.AllowedIPs), "allowed IPs")
block := uapiPeerBlock(t, c, peerKey)
assert.Contains(t, block, "endpoint=192.0.2.10:51820", "the dump must show the endpoint")
assert.Contains(t, block, "preshared_key="+hex.EncodeToString(psk[:]), "the dump must show the preshared key")
assert.Contains(t, block, "persistent_keepalive_interval=25", "the dump must show the keepalive")
assert.Contains(t, block, "allowed_ip=100.64.0.1/32", "the dump must show the first prefix")
assert.Contains(t, block, "allowed_ip=10.20.0.0/16", "the dump must show the second prefix")
}
func TestUpdatePeerKeepsTheEndpointZone(t *testing.T) {
c := newTestUSPConfigurer(t)
priv, err := wgtypes.GeneratePrivateKey()
require.NoError(t, err, "generate peer private key")
peerKey := priv.PublicKey().String()
// The idle device opens no socket, so the zone name does not have to exist on the host.
endpoint := &net.UDPAddr{IP: net.ParseIP("fe80::1"), Port: 51820, Zone: "eth0"}
prefixes := []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")}
require.NoError(t, c.UpdatePeer(peerKey, prefixes, 25*time.Second, endpoint, nil), "add peer")
assert.Contains(t, uapiPeerBlock(t, c, peerKey), "endpoint=[fe80::1%eth0]:51820",
"a link-local endpoint must keep its zone")
}
func TestRemoveEndpointAddressKeepsThePeerAndStopsTheKeepalive(t *testing.T) {
c := newTestUSPConfigurer(t)
priv, err := wgtypes.GeneratePrivateKey()
require.NoError(t, err, "generate peer private key")
peerKey := priv.PublicKey().String()
endpoint := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 10), Port: 51820}
prefixes := []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")}
require.NoError(t, c.UpdatePeer(peerKey, prefixes, 25*time.Second, endpoint, nil), "add peer")
before := c.device.LookupPeer(wgbackend.NoisePublicKey(priv.PublicKey()))
require.NotNil(t, before, "the peer must be on the device")
require.NoError(t, c.RemoveEndpointAddress(peerKey), "remove endpoint address")
assert.Same(t, before, c.device.LookupPeer(wgbackend.NoisePublicKey(priv.PublicKey())),
"the peer object must survive the endpoint removal")
stats, err := c.FullStats()
require.NoError(t, err, "read device stats")
require.Len(t, stats.Peers, 1, "the peer stays configured")
assert.Nil(t, stats.Peers[0].Endpoint.IP, "the endpoint must be gone")
assert.Equal(t, []string{"100.64.0.1/32"}, ipNetStrings(stats.Peers[0].AllowedIPs), "allowed IPs survive")
assert.Contains(t, uapiPeerBlock(t, c, peerKey), "persistent_keepalive_interval=0",
"an endpoint-less peer must not keep sending keepalives")
require.NoError(t, c.UpdatePeer(peerKey, prefixes, 25*time.Second, endpoint, nil), "restore the endpoint")
block := uapiPeerBlock(t, c, peerKey)
assert.Contains(t, block, "endpoint=192.0.2.10:51820", "the endpoint must be back")
assert.Contains(t, block, "persistent_keepalive_interval=25", "the keepalive must be back")
}
func TestUpdatePeerRejectsAnInvalidPrefix(t *testing.T) {
c := newTestUSPConfigurer(t)
priv, err := wgtypes.GeneratePrivateKey()
require.NoError(t, err, "generate peer private key")
peerKey := priv.PublicKey().String()
require.Error(t, c.UpdatePeer(peerKey, []netip.Prefix{{}}, 25*time.Second, nil, nil), "an invalid prefix must be rejected")
stats, err := c.FullStats()
require.NoError(t, err, "read device stats")
assert.Empty(t, stats.Peers, "the peer must not have reached the device")
}
func TestRemoveAllowedIPOnAbsentPeerReportsThePeer(t *testing.T) {
c := newTestUSPConfigurer(t)
priv, err := wgtypes.GeneratePrivateKey()
require.NoError(t, err, "generate peer private key")
assert.ErrorIs(t, c.RemoveAllowedIP(priv.PublicKey().String(), netip.MustParsePrefix("10.20.0.0/16")), ErrPeerNotFound,
"removing a prefix from a peer the device does not have must report the peer")
}
func ipNetStrings(ipNets []net.IPNet) []string {
out := make([]string, 0, len(ipNets))
for _, ipNet := range ipNets {
out = append(out, ipNet.String())
}
return out
}
// uapiPeerBlock reads the device back through the UAPI dump, so a typed write is checked
// against an independent view of the device.
func uapiPeerBlock(t *testing.T, c *WGUSPConfigurer, peerKey string) []string {
t.Helper()
parsed, err := wgtypes.ParseKey(peerKey)
require.NoError(t, err, "parse peer key")
wanted := "public_key=" + hex.EncodeToString(parsed[:])
dump, err := c.device.IpcGet()
require.NoError(t, err, "dump device")
var block []string
inBlock := false
for _, line := range strings.Split(strings.TrimSpace(dump), "\n") {
if strings.HasPrefix(line, "public_key=") {
inBlock = line == wanted
continue
}
if inBlock {
block = append(block, line)
}
}
require.NotEmpty(t, block, "peer %s must appear in the dump", peerKey)
return block
}