Files
netbird/client/iface/configurer/kernel_unix_test.go
T
Zoltan Papp 5c9c64e3e0 [client] Reuse one wgctrl client in the kernel configurer (#7912)
* [client] Reuse one wgctrl client in the kernel configurer

The kernel configurer opened a new wgctrl client for every request,
paying a socket open, a generic netlink family lookup and a close on
each peer update. With 10000 peers a single update dropped from about
37us to 10us per call once the client is kept open.

The configurer now opens the client lazily on the first request and
keeps it. A failed request drops the client so the next one opens a
fresh socket: a reply cut short mid-dump leaves stale messages in the
netlink buffer, and reusing that socket would fail every later request
on a sequence mismatch. The failed request is not retried.

Close releases the client and rejects later requests instead of
reopening a socket nobody would close. The kernel device never closed
its configurer, so it now keeps it and closes it on teardown.
2026-10-12 11:28:37 +02:00

168 lines
4.6 KiB
Go

//go:build (linux && !android) || freebsd
package configurer
import (
"errors"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type fakeWGClient struct {
configureErr error
deviceErr error
calls int
closed bool
}
type fakeWGClientFactory struct {
openErr error
clients []*fakeWGClient
}
func (f *fakeWGClient) Device(string) (*wgtypes.Device, error) {
f.calls++
if f.deviceErr != nil {
return nil, f.deviceErr
}
return &wgtypes.Device{}, nil
}
func (f *fakeWGClient) ConfigureDevice(string, wgtypes.Config) error {
f.calls++
return f.configureErr
}
func (f *fakeWGClient) Close() error {
f.closed = true
return nil
}
func (f *fakeWGClientFactory) open() (wgClient, error) {
if f.openErr != nil {
return nil, f.openErr
}
client := &fakeWGClient{}
f.clients = append(f.clients, client)
return client, nil
}
func TestKernelConfigurer_ReusesClientAcrossRequests(t *testing.T) {
c, factory := newFakeKernelConfigurer()
peerKey := testPeerKey(t)
require.NoError(t, c.RemovePeer(peerKey))
require.NoError(t, c.RemovePeer(peerKey))
_, err := c.FullStats()
require.NoError(t, err)
require.Len(t, factory.clients, 1, "all requests should share one client")
assert.Equal(t, 3, factory.clients[0].calls, "every request should go through the shared client")
assert.False(t, factory.clients[0].closed, "a healthy client should stay open")
}
func TestKernelConfigurer_ReopensClientAfterFailedConfigure(t *testing.T) {
c, factory := newFakeKernelConfigurer()
peerKey := testPeerKey(t)
require.NoError(t, c.RemovePeer(peerKey))
require.Len(t, factory.clients, 1)
factory.clients[0].configureErr = errors.New("netlink down")
require.Error(t, c.RemovePeer(peerKey))
assert.True(t, factory.clients[0].closed, "the failed client should be closed")
assert.Len(t, factory.clients, 1, "a failure must not open a client on its own")
require.NoError(t, c.RemovePeer(peerKey))
require.Len(t, factory.clients, 2, "the next request should open a fresh client")
assert.Equal(t, 1, factory.clients[1].calls, "the request should run on the fresh client")
assert.False(t, factory.clients[1].closed, "the fresh client should stay open")
}
func TestKernelConfigurer_ReopensClientAfterFailedDeviceQuery(t *testing.T) {
c, factory := newFakeKernelConfigurer()
_, err := c.FullStats()
require.NoError(t, err)
require.Len(t, factory.clients, 1)
factory.clients[0].deviceErr = errors.New("netlink down")
_, err = c.FullStats()
require.Error(t, err)
assert.True(t, factory.clients[0].closed, "the failed client should be closed")
_, err = c.FullStats()
require.NoError(t, err)
assert.Len(t, factory.clients, 2, "the next request should open a fresh client")
}
func TestKernelConfigurer_RetriesOpenOnNextRequest(t *testing.T) {
c, factory := newFakeKernelConfigurer()
peerKey := testPeerKey(t)
factory.openErr = errors.New("no netlink")
require.Error(t, c.RemovePeer(peerKey))
assert.Empty(t, factory.clients, "a failed open leaves no client behind")
factory.openErr = nil
require.NoError(t, c.RemovePeer(peerKey))
assert.Len(t, factory.clients, 1, "the next request should open the client")
}
func TestKernelConfigurer_CloseRejectsFurtherRequests(t *testing.T) {
c, factory := newFakeKernelConfigurer()
peerKey := testPeerKey(t)
require.NoError(t, c.RemovePeer(peerKey))
require.Len(t, factory.clients, 1)
c.Close()
c.Close()
assert.True(t, factory.clients[0].closed, "Close should release the client")
require.ErrorIs(t, c.RemovePeer(peerKey), errConfigurerClosed)
_, err := c.FullStats()
require.ErrorIs(t, err, errConfigurerClosed)
assert.Len(t, factory.clients, 1, "a closed configurer must not open a client")
}
func TestKernelConfigurer_ConcurrentRequestsShareOneClient(t *testing.T) {
c, factory := newFakeKernelConfigurer()
peerKey := testPeerKey(t)
const workers = 16
errs := make(chan error, workers)
var wg sync.WaitGroup
for range workers {
wg.Add(1)
go func() {
defer wg.Done()
errs <- c.RemovePeer(peerKey)
}()
}
wg.Wait()
close(errs)
for err := range errs {
require.NoError(t, err)
}
require.Len(t, factory.clients, 1, "concurrent requests should share one client")
assert.Equal(t, workers, factory.clients[0].calls, "every request should reach the shared client")
}
func newFakeKernelConfigurer() (*KernelConfigurer, *fakeWGClientFactory) {
factory := &fakeWGClientFactory{}
return newKernelConfigurer("wt0", factory.open), factory
}
func testPeerKey(t *testing.T) string {
t.Helper()
key, err := wgtypes.GeneratePrivateKey()
require.NoError(t, err)
return key.PublicKey().String()
}