Files
netbird/client/iface/configurer/usp.go
T
2026-10-16 18:24:14 +02:00

331 lines
9.1 KiB
Go

package configurer
import (
"fmt"
"math"
"net"
"net/netip"
"os"
"runtime"
"time"
log "github.com/sirupsen/logrus"
wgconn "golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/iface/bind"
nbnet "github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/monotime"
)
var ErrAllowedIPNotFound = fmt.Errorf("allowed IP not found")
type WGUSPConfigurer struct {
device *device.Device
deviceName string
activityRecorder *bind.ActivityRecorder
statsCache *statsCache
uapiListener net.Listener
}
// NewUSPConfigurer creates a userspace configurer and starts its UAPI listener.
func NewUSPConfigurer(device *device.Device, deviceName string, activityRecorder *bind.ActivityRecorder) *WGUSPConfigurer {
wgCfg := NewUSPConfigurerNoUAPI(device, deviceName, activityRecorder)
wgCfg.startUAPI()
return wgCfg
}
// NewUSPConfigurerNoUAPI creates a userspace configurer without a UAPI listener.
func NewUSPConfigurerNoUAPI(device *device.Device, deviceName string, activityRecorder *bind.ActivityRecorder) *WGUSPConfigurer {
wgCfg := &WGUSPConfigurer{
device: device,
deviceName: deviceName,
activityRecorder: activityRecorder,
}
wgCfg.statsCache = newStatsCache(statsCacheTTL, wgCfg.fetchStats)
return wgCfg
}
// ConfigureInterface sets the device key, port and firewall mark, replacing all peers.
func (c *WGUSPConfigurer) ConfigureInterface(privateKey string, port int) error {
log.Debugf("adding Wireguard private key")
key, err := wgtypes.ParseKey(privateKey)
if err != nil {
return err
}
if port < 0 || port > math.MaxUint16 {
return fmt.Errorf("invalid listen port %d", port)
}
if err := c.device.SetPrivateKey(device.NoisePrivateKey(key)); err != nil {
return fmt.Errorf("set private key: %w", err)
}
if err := c.device.SetListenPort(uint16(port)); err != nil {
return fmt.Errorf("set listen port %d: %w", port, err)
}
c.device.RemoveAllPeers()
if err := c.device.BindSetMark(uint32(getFwmark())); err != nil {
return fmt.Errorf("set fwmark: %w", err)
}
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 *WGUSPConfigurer) SetPresharedKey(peerKey string, psk wgtypes.Key, updateOnly bool) error {
parsedPeerKey, err := wgtypes.ParseKey(peerKey)
if err != nil {
return err
}
presharedKey := device.NoisePresharedKey(psk)
return c.device.ConfigurePeer(device.NoisePublicKey(parsedPeerKey), device.PeerConfig{
PresharedKey: &presharedKey,
UpdateOnly: updateOnly,
})
}
// UpdatePeer creates or updates a peer, merging allowed IPs with its existing set.
func (c *WGUSPConfigurer) 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
}
prefixes, err := validPrefixes(allowedIps)
if err != nil {
return err
}
cfg := device.PeerConfig{
PersistentKeepalive: &keepAlive,
AllowedIPs: prefixes,
}
var addrPort netip.AddrPort
if endpoint != nil {
addr, ok := netip.AddrFromSlice(endpoint.IP)
if !ok {
return fmt.Errorf("parse endpoint address %v", endpoint.IP)
}
addr = addr.Unmap()
addrPort = netip.AddrPortFrom(addr, uint16(endpoint.Port))
cfg.Endpoint = &bind.Endpoint{AddrPort: netip.AddrPortFrom(addr.WithZone(endpoint.Zone), addrPort.Port())}
}
if preSharedKey != nil {
psk := device.NoisePresharedKey(*preSharedKey)
cfg.PresharedKey = &psk
}
if err := c.device.ConfigurePeer(device.NoisePublicKey(peerKeyParsed), cfg); err != nil {
return fmt.Errorf("configure peer: %w", err)
}
if endpoint != nil {
c.activityRecorder.UpsertAddress(peerKey, addrPort)
}
return nil
}
// RemoveEndpointAddress clears the endpoint of a peer while keeping it configured.
// The session is reset and the keepalive is switched off until the next endpoint arrives.
func (c *WGUSPConfigurer) RemoveEndpointAddress(peerKey string) error {
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
if err != nil {
return fmt.Errorf("parse peer key: %w", err)
}
peer := c.device.LookupPeer(device.NoisePublicKey(peerKeyParsed))
if peer == nil {
return ErrPeerNotFound
}
peer.ClearEndpoint()
peer.ZeroAndFlushAll()
var noKeepAlive time.Duration
if err := c.device.ConfigurePeer(device.NoisePublicKey(peerKeyParsed), device.PeerConfig{
PersistentKeepalive: &noKeepAlive,
UpdateOnly: true,
}); err != nil {
return fmt.Errorf("disable keepalive: %w", err)
}
return nil
}
// RemovePeer removes a peer and clears its activity record.
func (c *WGUSPConfigurer) RemovePeer(peerKey string) error {
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
if err != nil {
return err
}
c.device.RemovePeer(device.NoisePublicKey(peerKeyParsed))
c.activityRecorder.Remove(peerKey)
return nil
}
// AddAllowedIP adds a prefix to an existing peer; an absent peer is a silent no-op.
func (c *WGUSPConfigurer) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error {
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
if err != nil {
return err
}
if !allowedIP.IsValid() {
return fmt.Errorf("invalid allowed IP %v", allowedIP)
}
peer := c.device.LookupPeer(device.NoisePublicKey(peerKeyParsed))
if peer == nil {
return nil
}
peer.AddAllowedIP(normalizePrefix(allowedIP))
return nil
}
// RemoveAllowedIP removes a prefix while preserving the peer's other allowed IPs.
// It returns ErrAllowedIPNotFound if the prefix is not assigned to the peer.
func (c *WGUSPConfigurer) RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error {
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
if err != nil {
return fmt.Errorf("parse peer key: %w", err)
}
if !allowedIP.IsValid() {
return ErrAllowedIPNotFound
}
peer := c.device.LookupPeer(device.NoisePublicKey(peerKeyParsed))
if peer == nil {
return ErrPeerNotFound
}
if !peer.RemoveAllowedIP(normalizePrefix(allowedIP)) {
return ErrAllowedIPNotFound
}
return nil
}
func (c *WGUSPConfigurer) FullStats() (*Stats, error) {
peers := c.device.Peers()
stats := &Stats{
DeviceName: c.deviceName,
PublicKey: wgtypes.Key(c.device.PublicKey()).String(),
ListenPort: int(c.device.ListenPort()),
FWMark: int(c.device.FirewallMark()),
Peers: make([]Peer, 0, len(peers)),
}
for _, peer := range peers {
stats.Peers = append(stats.Peers, peerStats(peer))
}
return stats, nil
}
func (c *WGUSPConfigurer) LastActivities() map[string]monotime.Time {
return c.activityRecorder.GetLastActivities()
}
func (c *WGUSPConfigurer) GetStats() (map[string]WGStats, error) {
return c.statsCache.get()
}
func (c *WGUSPConfigurer) Close() {
if c.uapiListener != nil {
err := c.uapiListener.Close()
if err != nil {
log.Errorf("failed to close uapi listener: %v", err)
}
}
if runtime.GOOS == "linux" {
sockPath := "/var/run/wireguard/" + c.deviceName + ".sock"
if _, statErr := os.Stat(sockPath); statErr == nil {
_ = os.Remove(sockPath)
}
}
}
// startUAPI starts the UAPI listener for managing the WireGuard interface via external tool
func (c *WGUSPConfigurer) startUAPI() {
var err error
c.uapiListener, err = openUAPI(c.deviceName)
if err != nil {
log.Errorf("failed to open uapi listener: %v", err)
return
}
go func(uapi net.Listener) {
for {
uapiConn, uapiErr := uapi.Accept()
if uapiErr != nil {
log.Tracef("%s", uapiErr)
return
}
go func() {
c.device.IpcHandle(uapiConn)
}()
}
}(c.uapiListener)
}
func (c *WGUSPConfigurer) fetchStats() (map[string]WGStats, error) {
peers := c.device.Peers()
stats := make(map[string]WGStats, len(peers))
for _, peer := range peers {
stats[wgtypes.Key(peer.PublicKey()).String()] = WGStats{
LastHandshake: peer.LastHandshake(),
TxBytes: int64(peer.TxBytes()),
RxBytes: int64(peer.RxBytes()),
}
}
return stats, nil
}
func peerStats(peer *device.Peer) Peer {
stats := Peer{
PublicKey: wgtypes.Key(peer.PublicKey()).String(),
AllowedIPs: prefixesToIPNets(peer.AllowedIPs()),
TxBytes: int64(peer.TxBytes()),
RxBytes: int64(peer.RxBytes()),
LastHandshake: peer.LastHandshake(),
PresharedKey: [32]byte(peer.PresharedKey()),
}
if addrPort, ok := endpointAddrPort(peer.Endpoint()); ok {
stats.Endpoint = net.UDPAddr{IP: addrPort.Addr().AsSlice(), Port: int(addrPort.Port())}
}
return stats
}
func endpointAddrPort(endpoint wgconn.Endpoint) (netip.AddrPort, bool) {
if endpoint == nil {
return netip.AddrPort{}, false
}
if ep, ok := endpoint.(*bind.Endpoint); ok {
return ep.AddrPort, true
}
addrPort, err := netip.ParseAddrPort(endpoint.DstToString())
if err != nil {
return netip.AddrPort{}, false
}
return addrPort, true
}
func validPrefixes(prefixes []netip.Prefix) ([]netip.Prefix, error) {
for _, prefix := range prefixes {
if !prefix.IsValid() {
return nil, fmt.Errorf("invalid allowed IP %v", prefix)
}
}
return normalizePrefixes(prefixes), nil
}
func getFwmark() int {
if nbnet.AdvancedRouting() && runtime.GOOS == "linux" {
return int(nbnet.ControlPlaneMark)
}
return 0
}