mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-14 01:19:09 +02:00
* [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.
168 lines
4.6 KiB
Go
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()
|
|
}
|