mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-13 17:09:07 +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.
420 lines
11 KiB
Go
420 lines
11 KiB
Go
//go:build (linux && !android) || freebsd
|
|
|
|
package configurer
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.zx2c4.com/wireguard/wgctrl"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
|
|
"github.com/netbirdio/netbird/monotime"
|
|
)
|
|
|
|
var errConfigurerClosed = errors.New("configurer closed")
|
|
|
|
type wgClient interface {
|
|
Device(name string) (*wgtypes.Device, error)
|
|
ConfigureDevice(name string, cfg wgtypes.Config) error
|
|
Close() error
|
|
}
|
|
|
|
type KernelConfigurer struct {
|
|
deviceName string
|
|
statsCache *statsCache
|
|
allowedIPs *allowedIPStore
|
|
newWgctrlFn func() (wgClient, error)
|
|
|
|
clientMu sync.Mutex
|
|
client wgClient
|
|
closed bool
|
|
}
|
|
|
|
// NewKernelConfigurer creates a configurer with an empty allowed IP mirror
|
|
// and a statistics cache for the named kernel device.
|
|
func NewKernelConfigurer(deviceName string) *KernelConfigurer {
|
|
return newKernelConfigurer(deviceName, func() (wgClient, error) {
|
|
return wgctrl.New()
|
|
})
|
|
}
|
|
|
|
func newKernelConfigurer(deviceName string, newWgctrlFn func() (wgClient, error)) *KernelConfigurer {
|
|
c := &KernelConfigurer{
|
|
deviceName: deviceName,
|
|
allowedIPs: newAllowedIPStore(),
|
|
newWgctrlFn: newWgctrlFn,
|
|
}
|
|
c.statsCache = newStatsCache(statsCacheTTL, c.fetchStats)
|
|
return c
|
|
}
|
|
|
|
// ConfigureInterface sets the device key, port and firewall mark, replacing all peers.
|
|
// The allowed IP mirror is reset only after the device accepts the configuration.
|
|
func (c *KernelConfigurer) ConfigureInterface(privateKey string, port int) error {
|
|
log.Debugf("adding Wireguard private key")
|
|
key, err := wgtypes.ParseKey(privateKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fwmark := getFwmark()
|
|
config := wgtypes.Config{
|
|
PrivateKey: &key,
|
|
ReplacePeers: true,
|
|
FirewallMark: &fwmark,
|
|
ListenPort: &port,
|
|
}
|
|
|
|
err = c.configure(config)
|
|
if err != nil {
|
|
return fmt.Errorf(`received error "%w" while configuring interface %s with port %d`, err, c.deviceName, port)
|
|
}
|
|
|
|
c.allowedIPs.reset()
|
|
return nil
|
|
}
|
|
|
|
// SetPresharedKey sets the preshared key for a peer.
|
|
// If updateOnly is true, only updates the existing peer; if false, creates or updates.
|
|
func (c *KernelConfigurer) SetPresharedKey(peerKey string, psk wgtypes.Key, updateOnly bool) error {
|
|
parsedPeerKey, err := wgtypes.ParseKey(peerKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg := buildPresharedKeyConfig(parsedPeerKey, psk, updateOnly)
|
|
if err := c.configure(cfg); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Without updateOnly this creates the peer when it is absent, so the store has to
|
|
// know about it even though no allowed IP was configured.
|
|
if !updateOnly {
|
|
c.allowedIPs.ensure(parsedPeerKey)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdatePeer creates or updates a peer, merging allowed IPs with its existing set.
|
|
// Prefixes assigned to this peer are transferred from their previous owners.
|
|
func (c *KernelConfigurer) UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
|
|
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
peer := wgtypes.PeerConfig{
|
|
PublicKey: peerKeyParsed,
|
|
ReplaceAllowedIPs: false,
|
|
// don't replace allowed ips, wg will handle duplicated peer IP
|
|
AllowedIPs: prefixesToIPNets(allowedIps),
|
|
PersistentKeepaliveInterval: &keepAlive,
|
|
Endpoint: endpoint,
|
|
PresharedKey: preSharedKey,
|
|
}
|
|
|
|
config := wgtypes.Config{
|
|
Peers: []wgtypes.PeerConfig{peer},
|
|
}
|
|
err = c.configure(config)
|
|
if err != nil {
|
|
return fmt.Errorf(`received error "%w" while updating peer on interface %s with settings: allowed ips %s, endpoint %s`, err, c.deviceName, allowedIps, endpoint.String())
|
|
}
|
|
|
|
c.allowedIPs.add(peerKeyParsed, allowedIps)
|
|
return nil
|
|
}
|
|
|
|
// RemoveEndpointAddress clears the endpoint of a peer while keeping it configured.
|
|
// Neither the netlink API nor the userspace one can clear an endpoint in place, so the peer
|
|
// is removed and re-added with the allowed IPs it already had.
|
|
func (c *KernelConfigurer) RemoveEndpointAddress(peerKey string) error {
|
|
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
allowedIPs, err := c.peerAllowedIPs(peerKeyParsed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
removePeerCfg := wgtypes.PeerConfig{
|
|
PublicKey: peerKeyParsed,
|
|
Remove: true,
|
|
}
|
|
|
|
if err := c.configure(wgtypes.Config{Peers: []wgtypes.PeerConfig{removePeerCfg}}); err != nil {
|
|
return fmt.Errorf("remove peer %s from interface %s: %w", peerKey, c.deviceName, err)
|
|
}
|
|
|
|
reAddPeerCfg := wgtypes.PeerConfig{
|
|
PublicKey: peerKeyParsed,
|
|
AllowedIPs: prefixesToIPNets(allowedIPs),
|
|
ReplaceAllowedIPs: true,
|
|
}
|
|
|
|
if err := c.configure(wgtypes.Config{Peers: []wgtypes.PeerConfig{reAddPeerCfg}}); err != nil {
|
|
c.allowedIPs.forget(peerKeyParsed)
|
|
return fmt.Errorf(
|
|
"re-add peer %s to interface %s with allowed IPs %v: %w",
|
|
peerKey, c.deviceName, allowedIPs, err,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemovePeer removes a peer and forgets its allowed IPs after a successful device write.
|
|
func (c *KernelConfigurer) RemovePeer(peerKey string) error {
|
|
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
peer := wgtypes.PeerConfig{
|
|
PublicKey: peerKeyParsed,
|
|
Remove: true,
|
|
}
|
|
|
|
config := wgtypes.Config{
|
|
Peers: []wgtypes.PeerConfig{peer},
|
|
}
|
|
err = c.configure(config)
|
|
if err != nil {
|
|
return fmt.Errorf(`received error "%w" while removing peer %s from interface %s`, err, peerKey, c.deviceName)
|
|
}
|
|
|
|
c.allowedIPs.forget(peerKeyParsed)
|
|
return nil
|
|
}
|
|
|
|
// AddAllowedIP adds a prefix to an existing peer; an absent peer is a silent no-op.
|
|
func (c *KernelConfigurer) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error {
|
|
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
peer := wgtypes.PeerConfig{
|
|
PublicKey: peerKeyParsed,
|
|
UpdateOnly: true,
|
|
ReplaceAllowedIPs: false,
|
|
AllowedIPs: prefixesToIPNets([]netip.Prefix{allowedIP}),
|
|
}
|
|
|
|
config := wgtypes.Config{
|
|
Peers: []wgtypes.PeerConfig{peer},
|
|
}
|
|
err = c.configure(config)
|
|
if err != nil {
|
|
return fmt.Errorf(`received error "%w" while adding allowed Ip to peer on interface %s with settings: allowed ips %s`, err, c.deviceName, allowedIP)
|
|
}
|
|
|
|
c.allowedIPs.addExisting(peerKeyParsed, []netip.Prefix{allowedIP})
|
|
return nil
|
|
}
|
|
|
|
// RemoveAllowedIP removes a prefix while preserving the peer's other allowed IPs.
|
|
// A prefix not assigned to the peer is a no-op.
|
|
func (c *KernelConfigurer) RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error {
|
|
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
|
if err != nil {
|
|
return fmt.Errorf("parse peer key: %w", err)
|
|
}
|
|
|
|
currentAllowedIPs, err := c.peerAllowedIPs(peerKeyParsed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
idx := slices.Index(currentAllowedIPs, normalizePrefix(allowedIP))
|
|
if idx < 0 {
|
|
return nil
|
|
}
|
|
newAllowedIPs := slices.Delete(currentAllowedIPs, idx, idx+1)
|
|
|
|
peer := wgtypes.PeerConfig{
|
|
PublicKey: peerKeyParsed,
|
|
UpdateOnly: true,
|
|
ReplaceAllowedIPs: true,
|
|
AllowedIPs: prefixesToIPNets(newAllowedIPs),
|
|
}
|
|
|
|
config := wgtypes.Config{
|
|
Peers: []wgtypes.PeerConfig{peer},
|
|
}
|
|
if err := c.configure(config); err != nil {
|
|
return fmt.Errorf("remove allowed IP %s on interface %s: %w", allowedIP, c.deviceName, err)
|
|
}
|
|
|
|
c.allowedIPs.set(peerKeyParsed, newAllowedIPs)
|
|
return nil
|
|
}
|
|
|
|
// peerAllowedIPs returns the allowed IPs configured for a peer, reading them from the device
|
|
// only for a peer the store has not seen. Dumping the device costs a netlink round trip
|
|
// proportional to the whole network map, and this runs on every relay and ICE transition.
|
|
func (c *KernelConfigurer) peerAllowedIPs(peerKey wgtypes.Key) ([]netip.Prefix, error) {
|
|
if prefixes, ok := c.allowedIPs.get(peerKey); ok {
|
|
return prefixes, nil
|
|
}
|
|
|
|
existingPeer, err := c.getPeer(peerKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get peer: %w", err)
|
|
}
|
|
|
|
prefixes := ipNetsToPrefixes(existingPeer.AllowedIPs)
|
|
c.allowedIPs.set(peerKey, prefixes)
|
|
return prefixes, nil
|
|
}
|
|
|
|
// getPeer scans the device for one peer. wgtypes.Key is an array, so the comparison is a
|
|
// plain equality: Key.String would base64 encode into a fresh allocation for every peer.
|
|
func (c *KernelConfigurer) getPeer(peerPubKey wgtypes.Key) (wgtypes.Peer, error) {
|
|
wgDevice, err := c.device()
|
|
if err != nil {
|
|
return wgtypes.Peer{}, err
|
|
}
|
|
for _, peer := range wgDevice.Peers {
|
|
if peer.PublicKey == peerPubKey {
|
|
return peer, nil
|
|
}
|
|
}
|
|
return wgtypes.Peer{}, ErrPeerNotFound
|
|
}
|
|
|
|
func (c *KernelConfigurer) configure(config wgtypes.Config) error {
|
|
c.clientMu.Lock()
|
|
defer c.clientMu.Unlock()
|
|
|
|
client, err := c.clientLocked()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := client.ConfigureDevice(c.deviceName, config); err != nil {
|
|
c.discardLocked()
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *KernelConfigurer) device() (*wgtypes.Device, error) {
|
|
c.clientMu.Lock()
|
|
defer c.clientMu.Unlock()
|
|
|
|
client, err := c.clientLocked()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wgDevice, err := client.Device(c.deviceName)
|
|
if err != nil {
|
|
c.discardLocked()
|
|
return nil, fmt.Errorf("get device %s: %w", c.deviceName, err)
|
|
}
|
|
return wgDevice, nil
|
|
}
|
|
|
|
func (c *KernelConfigurer) clientLocked() (wgClient, error) {
|
|
if c.closed {
|
|
return nil, errConfigurerClosed
|
|
}
|
|
if c.client != nil {
|
|
return c.client, nil
|
|
}
|
|
client, err := c.newWgctrlFn()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wgctl: %w", err)
|
|
}
|
|
c.client = client
|
|
return client, nil
|
|
}
|
|
|
|
func (c *KernelConfigurer) discardLocked() {
|
|
if c.client == nil {
|
|
return
|
|
}
|
|
if err := c.client.Close(); err != nil {
|
|
log.Warnf("failed to close wgctrl client: %v", err)
|
|
}
|
|
c.client = nil
|
|
}
|
|
|
|
func (c *KernelConfigurer) Close() {
|
|
c.clientMu.Lock()
|
|
defer c.clientMu.Unlock()
|
|
|
|
c.closed = true
|
|
c.discardLocked()
|
|
}
|
|
|
|
func (c *KernelConfigurer) FullStats() (*Stats, error) {
|
|
wgDevice, err := c.device()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fullStats := &Stats{
|
|
DeviceName: wgDevice.Name,
|
|
PublicKey: wgDevice.PublicKey.String(),
|
|
ListenPort: wgDevice.ListenPort,
|
|
FWMark: wgDevice.FirewallMark,
|
|
Peers: []Peer{},
|
|
}
|
|
|
|
for _, p := range wgDevice.Peers {
|
|
peer := Peer{
|
|
PublicKey: p.PublicKey.String(),
|
|
AllowedIPs: p.AllowedIPs,
|
|
TxBytes: p.TransmitBytes,
|
|
RxBytes: p.ReceiveBytes,
|
|
LastHandshake: p.LastHandshakeTime,
|
|
PresharedKey: [32]byte(p.PresharedKey),
|
|
}
|
|
if p.Endpoint != nil {
|
|
peer.Endpoint = *p.Endpoint
|
|
}
|
|
fullStats.Peers = append(fullStats.Peers, peer)
|
|
}
|
|
return fullStats, nil
|
|
}
|
|
|
|
func (c *KernelConfigurer) GetStats() (map[string]WGStats, error) {
|
|
return c.statsCache.get()
|
|
}
|
|
|
|
func (c *KernelConfigurer) LastActivities() map[string]monotime.Time {
|
|
return nil
|
|
}
|
|
|
|
func (c *KernelConfigurer) fetchStats() (map[string]WGStats, error) {
|
|
wgDevice, err := c.device()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats := make(map[string]WGStats)
|
|
for _, peer := range wgDevice.Peers {
|
|
stats[peer.PublicKey.String()] = WGStats{
|
|
LastHandshake: peer.LastHandshakeTime,
|
|
TxBytes: peer.TransmitBytes,
|
|
RxBytes: peer.ReceiveBytes,
|
|
}
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
func buildPresharedKeyConfig(peerKey wgtypes.Key, psk wgtypes.Key, updateOnly bool) wgtypes.Config {
|
|
return wgtypes.Config{
|
|
Peers: []wgtypes.PeerConfig{{
|
|
PublicKey: peerKey,
|
|
PresharedKey: &psk,
|
|
UpdateOnly: updateOnly,
|
|
}},
|
|
}
|
|
}
|