mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-13 17:09:07 +02:00
* [client] Configure the userspace device through its typed API The userspace configurer drove wireguard-go through UAPI text: every write was rendered to the configuration protocol and parsed back, and every read, including the per-second statistics poll, dumped the whole device as text and parsed it again. With 10000 peers a statistics read cost about 16 ms, and clearing one endpoint needed a peer remove and a re-add because the protocol cannot clear an endpoint or drop a single prefix in place. The allowed IP mirror in the configurer existed only to feed those rewrites. The wireguard-go fork now exposes a typed peer API, so the configurer calls the device directly: ConfigurePeer for creates and updates, single-prefix add and remove on the routing table, ClearEndpoint, and accessors for statistics. The mirror, the UAPI rendering and the parsers are gone from the userspace path; the UAPI socket for wg show stays. Clearing an endpoint keeps the peer object, its counters and its allowed IPs, but resets the session and switches the keepalive off until the next endpoint arrives. The watcher expects a fresh handshake after an endpoint reset, and an endpoint-less peer with a keepalive would retry handshakes it cannot send; this is what the remove and re-add provided before. Handshake times now carry nanosecond precision like the kernel path. Measured with 10000 peers against the UAPI implementation: clearing an endpoint drops from 713 ms to 3 ms for all peers, a statistics read from 15 ms to 0.9 ms, a full dump from 18 ms to 4 ms, and single allowed IP changes are 7 to 20 times faster. * [client] Keep the allowed IP mirror kernel-only The userspace configurer no longer uses the allowed IP mirror, so its ensure method and the preshared key config helper had no caller left on Windows and macOS, where the kernel configurer is not compiled, and the unused linter failed there. The mirror and its tests now carry the kernel build tag, the prefix normalizers that both paths share move to common.go, and the preshared key helper moves next to its only caller. * [client] Trim historical references from configurer comments * [client] Keep the endpoint zone in the userspace configurer The UAPI path wrote the endpoint as text, so a link-local IPv6 endpoint reached the device with its zone. The typed path built the address from the IP bytes only and dropped the zone, leaving the device with an unroutable fe80:: address. The activity recorder keeps the zone-less address it always had. * [client] Run the prefix conversion tests on every platform prefixesToIPNets lives in the untagged common.go and the userspace stats path uses it on every platform, but its tests stayed in the kernel-tagged allowedips_test.go, so darwin and windows CI stopped running them. Move them next to the normalizePrefix tests. The round-trip test stays tagged because ipNetsToPrefixes is kernel-only. * [client] Pin wireguard-go to the merged typed peer API The typed peer configuration API landed on master as #23, so the replace no longer needs to point at the PR branch commit.
224 lines
8.2 KiB
Go
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"
|
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
|
"golang.zx2c4.com/wireguard/tun"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
)
|
|
|
|
// 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 tun.Event
|
|
closed chan struct{}
|
|
}
|
|
|
|
func newIdleTUN() *idleTUN {
|
|
return &idleTUN{events: make(chan tun.Event), 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 tun.Event { 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(wgdevice.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(wgdevice.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
|
|
}
|