mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-21 21:09:07 +02:00
331 lines
9.1 KiB
Go
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
|
|
}
|