diff --git a/client/iface/configurer/common.go b/client/iface/configurer/common.go index 088cff69d..10162d703 100644 --- a/client/iface/configurer/common.go +++ b/client/iface/configurer/common.go @@ -3,8 +3,22 @@ package configurer import ( "net" "net/netip" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) +// buildPresharedKeyConfig creates a wgtypes.Config for setting a preshared key on a peer. +// This is a shared helper used by both kernel and userspace configurers. +func buildPresharedKeyConfig(peerKey wgtypes.Key, psk wgtypes.Key, updateOnly bool) wgtypes.Config { + return wgtypes.Config{ + Peers: []wgtypes.PeerConfig{{ + PublicKey: peerKey, + PresharedKey: &psk, + UpdateOnly: updateOnly, + }}, + } +} + func prefixesToIPNets(prefixes []netip.Prefix) []net.IPNet { ipNets := make([]net.IPNet, len(prefixes)) for i, prefix := range prefixes { diff --git a/client/iface/configurer/kernel_unix.go b/client/iface/configurer/kernel_unix.go index 96b286175..a29fe181a 100644 --- a/client/iface/configurer/kernel_unix.go +++ b/client/iface/configurer/kernel_unix.go @@ -15,8 +15,6 @@ import ( "github.com/netbirdio/netbird/monotime" ) -var zeroKey wgtypes.Key - type KernelConfigurer struct { deviceName string } @@ -48,6 +46,18 @@ func (c *KernelConfigurer) ConfigureInterface(privateKey string, port int) error 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) + return c.configure(cfg) +} + 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 { @@ -279,7 +289,7 @@ func (c *KernelConfigurer) FullStats() (*Stats, error) { TxBytes: p.TransmitBytes, RxBytes: p.ReceiveBytes, LastHandshake: p.LastHandshakeTime, - PresharedKey: p.PresharedKey != zeroKey, + PresharedKey: [32]byte(p.PresharedKey), } if p.Endpoint != nil { peer.Endpoint = *p.Endpoint diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go index bc875b73c..c4ea349df 100644 --- a/client/iface/configurer/usp.go +++ b/client/iface/configurer/usp.go @@ -22,17 +22,16 @@ import ( ) const ( - privateKey = "private_key" - ipcKeyLastHandshakeTimeSec = "last_handshake_time_sec" - ipcKeyLastHandshakeTimeNsec = "last_handshake_time_nsec" - ipcKeyTxBytes = "tx_bytes" - ipcKeyRxBytes = "rx_bytes" - allowedIP = "allowed_ip" - endpoint = "endpoint" - fwmark = "fwmark" - listenPort = "listen_port" - publicKey = "public_key" - presharedKey = "preshared_key" + privateKey = "private_key" + ipcKeyLastHandshakeTimeSec = "last_handshake_time_sec" + ipcKeyTxBytes = "tx_bytes" + ipcKeyRxBytes = "rx_bytes" + allowedIP = "allowed_ip" + endpoint = "endpoint" + fwmark = "fwmark" + listenPort = "listen_port" + publicKey = "public_key" + presharedKey = "preshared_key" ) var ErrAllowedIPNotFound = fmt.Errorf("allowed IP not found") @@ -72,6 +71,18 @@ func (c *WGUSPConfigurer) ConfigureInterface(privateKey string, port int) error return c.device.IpcSet(toWgUserspaceString(config)) } +// 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 + } + + cfg := buildPresharedKeyConfig(parsedPeerKey, psk, updateOnly) + return c.device.IpcSet(toWgUserspaceString(cfg)) +} + 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 { @@ -422,23 +433,19 @@ func toWgUserspaceString(wgCfg wgtypes.Config) string { hexKey := hex.EncodeToString(p.PublicKey[:]) sb.WriteString(fmt.Sprintf("public_key=%s\n", hexKey)) + if p.Remove { + sb.WriteString("remove=true\n") + } + + if p.UpdateOnly { + sb.WriteString("update_only=true\n") + } + if p.PresharedKey != nil { preSharedHexKey := hex.EncodeToString(p.PresharedKey[:]) sb.WriteString(fmt.Sprintf("preshared_key=%s\n", preSharedHexKey)) } - if p.Remove { - sb.WriteString("remove=true") - } - - if p.ReplaceAllowedIPs { - sb.WriteString("replace_allowed_ips=true\n") - } - - for _, aip := range p.AllowedIPs { - sb.WriteString(fmt.Sprintf("allowed_ip=%s\n", aip.String())) - } - if p.Endpoint != nil { sb.WriteString(fmt.Sprintf("endpoint=%s\n", p.Endpoint.String())) } @@ -446,6 +453,14 @@ func toWgUserspaceString(wgCfg wgtypes.Config) string { if p.PersistentKeepaliveInterval != nil { sb.WriteString(fmt.Sprintf("persistent_keepalive_interval=%d\n", int(p.PersistentKeepaliveInterval.Seconds()))) } + + if p.ReplaceAllowedIPs { + sb.WriteString("replace_allowed_ips=true\n") + } + + for _, aip := range p.AllowedIPs { + sb.WriteString(fmt.Sprintf("allowed_ip=%s\n", aip.String())) + } } return sb.String() } @@ -599,7 +614,9 @@ func parseStatus(deviceName, ipcStr string) (*Stats, error) { continue } if val != "" && val != "0000000000000000000000000000000000000000000000000000000000000000" { - currentPeer.PresharedKey = true + if pskKey, err := hexToWireguardKey(val); err == nil { + currentPeer.PresharedKey = [32]byte(pskKey) + } } } } diff --git a/client/iface/configurer/wgshow.go b/client/iface/configurer/wgshow.go index 604264026..4a5c31160 100644 --- a/client/iface/configurer/wgshow.go +++ b/client/iface/configurer/wgshow.go @@ -12,7 +12,7 @@ type Peer struct { TxBytes int64 RxBytes int64 LastHandshake time.Time - PresharedKey bool + PresharedKey [32]byte } type Stats struct { diff --git a/client/iface/device/interface.go b/client/iface/device/interface.go index db53d9c3a..7bab7b757 100644 --- a/client/iface/device/interface.go +++ b/client/iface/device/interface.go @@ -17,6 +17,7 @@ type WGConfigurer interface { RemovePeer(peerKey string) error AddAllowedIP(peerKey string, allowedIP netip.Prefix) error RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error + SetPresharedKey(peerKey string, psk wgtypes.Key, updateOnly bool) error Close() GetStats() (map[string]configurer.WGStats, error) FullStats() (*configurer.Stats, error) diff --git a/client/iface/iface.go b/client/iface/iface.go index 07235a995..71fd433ad 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -297,6 +297,19 @@ func (w *WGIface) FullStats() (*configurer.Stats, error) { return w.configurer.FullStats() } +// SetPresharedKey sets or updates the preshared key for a peer. +// If updateOnly is true, only updates existing peer; if false, creates or updates. +func (w *WGIface) SetPresharedKey(peerKey string, psk wgtypes.Key, updateOnly bool) error { + w.mu.Lock() + defer w.mu.Unlock() + + if w.configurer == nil { + return ErrIfaceNotFound + } + + return w.configurer.SetPresharedKey(peerKey, psk, updateOnly) +} + func (w *WGIface) waitUntilRemoved() error { maxWaitTime := 5 * time.Second timeout := time.NewTimer(maxWaitTime) diff --git a/client/internal/debug/wgshow.go b/client/internal/debug/wgshow.go index 8233ca510..1e8a8a6cc 100644 --- a/client/internal/debug/wgshow.go +++ b/client/internal/debug/wgshow.go @@ -60,7 +60,7 @@ func (g *BundleGenerator) toWGShowFormat(s *configurer.Stats) string { } sb.WriteString(fmt.Sprintf(" latest handshake: %s\n", peer.LastHandshake.Format(time.RFC1123))) sb.WriteString(fmt.Sprintf(" transfer: %d B received, %d B sent\n", peer.RxBytes, peer.TxBytes)) - if peer.PresharedKey { + if peer.PresharedKey != [32]byte{} { sb.WriteString(" preshared key: (hidden)\n") } } diff --git a/client/internal/engine.go b/client/internal/engine.go index c5e2b7c6c..25a4e4048 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -505,6 +505,11 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL) return fmt.Errorf("up wg interface: %w", err) } + // Set the WireGuard interface for rosenpass after interface is up + if e.rpManager != nil { + e.rpManager.SetInterface(e.wgInterface) + } + // if inbound conns are blocked there is no need to create the ACL manager if e.firewall != nil && !e.config.BlockInbound { e.acl = acl.NewDefaultManager(e.firewall) @@ -1512,6 +1517,7 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentV if e.rpManager != nil { peerConn.SetOnConnected(e.rpManager.OnConnected) peerConn.SetOnDisconnected(e.rpManager.OnDisconnected) + peerConn.SetRosenpassInitializedPresharedKeyValidator(e.rpManager.IsPresharedKeyInitialized) } return peerConn, nil diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 56829393c..af9f27a71 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -214,6 +214,10 @@ func (m *MockWGIface) LastActivities() map[string]monotime.Time { return nil } +func (m *MockWGIface) SetPresharedKey(peerKey string, psk wgtypes.Key, updateOnly bool) error { + return nil +} + func TestMain(m *testing.M) { _ = util.InitLog("debug", util.LogConsole) code := m.Run() diff --git a/client/internal/iface_common.go b/client/internal/iface_common.go index 90b06cbd1..f8a433a6e 100644 --- a/client/internal/iface_common.go +++ b/client/internal/iface_common.go @@ -42,4 +42,5 @@ type wgIfaceBase interface { GetNet() *netstack.Net FullStats() (*configurer.Stats, error) LastActivities() map[string]monotime.Time + SetPresharedKey(peerKey string, psk wgtypes.Key, updateOnly bool) error } diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 80ca36789..ba82354a2 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -88,8 +88,9 @@ type Conn struct { relayManager *relayClient.Manager srWatcher *guard.SRWatcher - onConnected func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string) - onDisconnected func(remotePeer string) + onConnected func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string) + onDisconnected func(remotePeer string) + rosenpassInitializedPresharedKeyValidator func(peerKey string) bool statusRelay *worker.AtomicWorkerStatus statusICE *worker.AtomicWorkerStatus @@ -289,6 +290,13 @@ func (conn *Conn) SetOnDisconnected(handler func(remotePeer string)) { conn.onDisconnected = handler } +// SetRosenpassInitializedPresharedKeyValidator sets a function to check if Rosenpass has taken over +// PSK management for a peer. When this returns true, presharedKey() returns nil +// to prevent UpdatePeer from overwriting the Rosenpass-managed PSK. +func (conn *Conn) SetRosenpassInitializedPresharedKeyValidator(handler func(peerKey string) bool) { + conn.rosenpassInitializedPresharedKeyValidator = handler +} + func (conn *Conn) OnRemoteOffer(offer OfferAnswer) { conn.dumpState.RemoteOffer() conn.Log.Infof("OnRemoteOffer, on status ICE: %s, status Relay: %s", conn.statusICE, conn.statusRelay) @@ -759,10 +767,24 @@ func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key { return conn.config.WgConfig.PreSharedKey } + // If Rosenpass has already set a PSK for this peer, return nil to prevent + // UpdatePeer from overwriting the Rosenpass-managed key. + if conn.rosenpassInitializedPresharedKeyValidator != nil && conn.rosenpassInitializedPresharedKeyValidator(conn.config.Key) { + return nil + } + + // Use NetBird PSK as the seed for Rosenpass. This same PSK is passed to + // Rosenpass as PeerConfig.PresharedKey, ensuring the derived post-quantum + // key is cryptographically bound to the original secret. + if conn.config.WgConfig.PreSharedKey != nil { + return conn.config.WgConfig.PreSharedKey + } + + // Fallback to deterministic key if no NetBird PSK is configured determKey, err := conn.rosenpassDetermKey() if err != nil { conn.Log.Errorf("failed to generate Rosenpass initial key: %v", err) - return conn.config.WgConfig.PreSharedKey + return nil } return determKey diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go index 6b47f95eb..32383b530 100644 --- a/client/internal/peer/conn_test.go +++ b/client/internal/peer/conn_test.go @@ -284,3 +284,27 @@ func TestConn_presharedKey(t *testing.T) { }) } } + +func TestConn_presharedKey_RosenpassManaged(t *testing.T) { + conn := Conn{ + config: ConnConfig{ + Key: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=", + LocalKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=", + RosenpassConfig: RosenpassConfig{PubKey: []byte("dummykey")}, + }, + } + + // When Rosenpass has already initialized the PSK for this peer, + // presharedKey must return nil to avoid UpdatePeer overwriting it. + conn.rosenpassInitializedPresharedKeyValidator = func(peerKey string) bool { return true } + if k := conn.presharedKey([]byte("remote")); k != nil { + t.Fatalf("expected nil presharedKey when Rosenpass manages PSK, got %v", k) + } + + // When Rosenpass hasn't taken over yet, presharedKey should provide + // a non-nil initial key (deterministic or from NetBird PSK). + conn.rosenpassInitializedPresharedKeyValidator = func(peerKey string) bool { return false } + if k := conn.presharedKey([]byte("remote")); k == nil { + t.Fatalf("expected non-nil presharedKey before Rosenpass manages PSK") + } +} diff --git a/client/internal/rosenpass/manager.go b/client/internal/rosenpass/manager.go index d2d7408fd..26a1eef58 100644 --- a/client/internal/rosenpass/manager.go +++ b/client/internal/rosenpass/manager.go @@ -34,6 +34,7 @@ type Manager struct { server *rp.Server lock sync.Mutex port int + wgIface PresharedKeySetter } // NewManager creates a new Rosenpass manager @@ -109,7 +110,13 @@ func (m *Manager) generateConfig() (rp.Config, error) { cfg.SecretKey = m.ssk cfg.Peers = []rp.PeerConfig{} - m.rpWgHandler, _ = NewNetbirdHandler(m.preSharedKey, m.ifaceName) + + m.lock.Lock() + m.rpWgHandler = NewNetbirdHandler() + if m.wgIface != nil { + m.rpWgHandler.SetInterface(m.wgIface) + } + m.lock.Unlock() cfg.Handlers = []rp.Handler{m.rpWgHandler} @@ -172,6 +179,20 @@ func (m *Manager) Close() error { return nil } +// SetInterface sets the WireGuard interface for the rosenpass handler. +// This can be called before or after Run() - the interface will be stored +// and passed to the handler when it's created or updated immediately if +// already running. +func (m *Manager) SetInterface(iface PresharedKeySetter) { + m.lock.Lock() + defer m.lock.Unlock() + + m.wgIface = iface + if m.rpWgHandler != nil { + m.rpWgHandler.SetInterface(iface) + } +} + // OnConnected is a handler function that is triggered when a connection to a remote peer establishes func (m *Manager) OnConnected(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string) { m.lock.Lock() @@ -192,6 +213,20 @@ func (m *Manager) OnConnected(remoteWireGuardKey string, remoteRosenpassPubKey [ } } +// IsPresharedKeyInitialized returns true if Rosenpass has completed a handshake +// and set a PSK for the given WireGuard peer. +func (m *Manager) IsPresharedKeyInitialized(wireGuardPubKey string) bool { + m.lock.Lock() + defer m.lock.Unlock() + + peerID, ok := m.rpPeerIDs[wireGuardPubKey] + if !ok || peerID == nil { + return false + } + + return m.rpWgHandler.IsPeerInitialized(*peerID) +} + func findRandomAvailableUDPPort() (int, error) { conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) if err != nil { diff --git a/client/internal/rosenpass/netbird_handler.go b/client/internal/rosenpass/netbird_handler.go index 345f95c01..9de2409ef 100644 --- a/client/internal/rosenpass/netbird_handler.go +++ b/client/internal/rosenpass/netbird_handler.go @@ -1,46 +1,50 @@ package rosenpass import ( - "fmt" - "log/slog" + "sync" rp "cunicu.li/go-rosenpass" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) +// PresharedKeySetter is the interface for setting preshared keys on WireGuard peers. +// This minimal interface allows rosenpass to update PSKs without depending on the full WGIface. +type PresharedKeySetter interface { + SetPresharedKey(peerKey string, psk wgtypes.Key, updateOnly bool) error +} + type wireGuardPeer struct { Interface string PublicKey rp.Key } type NetbirdHandler struct { - ifaceName string - client *wgctrl.Client - peers map[rp.PeerID]wireGuardPeer - presharedKey [32]byte + mu sync.Mutex + iface PresharedKeySetter + peers map[rp.PeerID]wireGuardPeer + initializedPeers map[rp.PeerID]bool } -func NewNetbirdHandler(preSharedKey *[32]byte, wgIfaceName string) (hdlr *NetbirdHandler, err error) { - hdlr = &NetbirdHandler{ - ifaceName: wgIfaceName, - peers: map[rp.PeerID]wireGuardPeer{}, +func NewNetbirdHandler() *NetbirdHandler { + return &NetbirdHandler{ + peers: map[rp.PeerID]wireGuardPeer{}, + initializedPeers: map[rp.PeerID]bool{}, } +} - if preSharedKey != nil { - hdlr.presharedKey = *preSharedKey - } - - if hdlr.client, err = wgctrl.New(); err != nil { - return nil, fmt.Errorf("failed to creat WireGuard client: %w", err) - } - - return hdlr, nil +// SetInterface sets the WireGuard interface for the handler. +// This must be called after the WireGuard interface is created. +func (h *NetbirdHandler) SetInterface(iface PresharedKeySetter) { + h.mu.Lock() + defer h.mu.Unlock() + h.iface = iface } func (h *NetbirdHandler) AddPeer(pid rp.PeerID, intf string, pk rp.Key) { + h.mu.Lock() + defer h.mu.Unlock() h.peers[pid] = wireGuardPeer{ Interface: intf, PublicKey: pk, @@ -48,79 +52,61 @@ func (h *NetbirdHandler) AddPeer(pid rp.PeerID, intf string, pk rp.Key) { } func (h *NetbirdHandler) RemovePeer(pid rp.PeerID) { + h.mu.Lock() + defer h.mu.Unlock() delete(h.peers, pid) + delete(h.initializedPeers, pid) +} + +// IsPeerInitialized returns true if Rosenpass has completed a handshake +// and set a PSK for this peer. +func (h *NetbirdHandler) IsPeerInitialized(pid rp.PeerID) bool { + h.mu.Lock() + defer h.mu.Unlock() + return h.initializedPeers[pid] } func (h *NetbirdHandler) HandshakeCompleted(pid rp.PeerID, key rp.Key) { - log.Debug("Handshake complete") h.outputKey(rp.KeyOutputReasonStale, pid, key) } func (h *NetbirdHandler) HandshakeExpired(pid rp.PeerID) { key, _ := rp.GeneratePresharedKey() - log.Debug("Handshake expired") h.outputKey(rp.KeyOutputReasonStale, pid, key) } func (h *NetbirdHandler) outputKey(_ rp.KeyOutputReason, pid rp.PeerID, psk rp.Key) { + h.mu.Lock() + iface := h.iface wg, ok := h.peers[pid] + isInitialized := h.initializedPeers[pid] + h.mu.Unlock() + + if iface == nil { + log.Warn("rosenpass: interface not set, cannot update preshared key") + return + } + if !ok { return } - device, err := h.client.Device(h.ifaceName) - if err != nil { - log.Errorf("Failed to get WireGuard device: %v", err) + peerKey := wgtypes.Key(wg.PublicKey).String() + pskKey := wgtypes.Key(psk) + + // Use updateOnly=true for later rotations (peer already has Rosenpass PSK) + // Use updateOnly=false for first rotation (peer has original/empty PSK) + if err := iface.SetPresharedKey(peerKey, pskKey, isInitialized); err != nil { + log.Errorf("Failed to apply rosenpass key: %v", err) return } - config := []wgtypes.PeerConfig{ - { - UpdateOnly: true, - PublicKey: wgtypes.Key(wg.PublicKey), - PresharedKey: (*wgtypes.Key)(&psk), - }, - } - for _, peer := range device.Peers { - if peer.PublicKey == wgtypes.Key(wg.PublicKey) { - if publicKeyEmpty(peer.PresharedKey) || peer.PresharedKey == h.presharedKey { - log.Debugf("Restart wireguard connection to peer %s", peer.PublicKey) - config = []wgtypes.PeerConfig{ - { - PublicKey: wgtypes.Key(wg.PublicKey), - PresharedKey: (*wgtypes.Key)(&psk), - Endpoint: peer.Endpoint, - AllowedIPs: peer.AllowedIPs, - }, - } - err = h.client.ConfigureDevice(wg.Interface, wgtypes.Config{ - Peers: []wgtypes.PeerConfig{ - { - Remove: true, - PublicKey: wgtypes.Key(wg.PublicKey), - }, - }, - }) - if err != nil { - slog.Debug("Failed to remove peer") - return - } - } + // Mark peer as isInitialized after the successful first rotation + if !isInitialized { + h.mu.Lock() + if _, exists := h.peers[pid]; exists { + h.initializedPeers[pid] = true } - } - - if err = h.client.ConfigureDevice(wg.Interface, wgtypes.Config{ - Peers: config, - }); err != nil { - log.Errorf("Failed to apply rosenpass key: %v", err) + h.mu.Unlock() } } - -func publicKeyEmpty(key wgtypes.Key) bool { - for _, b := range key { - if b != 0 { - return false - } - } - return true -}