From 30d15ecc3d9bf69161f8a8eda597acb78a29b602 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sat, 11 Jul 2026 11:03:55 +0200 Subject: [PATCH] [client,management] sync 0.74.4 changes (#6727) * [management] fix: prevent reverse proxy domain from being pushed as DNS search domain by @blaugrau90 in https://github.com/netbirdio/netbird/pull/6498 * [client] Recover from rosenpass key desync by @lixmal in https://github.com/netbirdio/netbird/pull/6714 * [client] Bump golang.org/x/crypto to v0.54.0 by @lixmal in https://github.com/netbirdio/netbird/pull/6709 * [client] fix MDM managementURL conflict on default-port URL echo by @riccardomanfrin in https://github.com/netbirdio/netbird/pull/6672 * [client] Update gopsutil to v4 by @mlsmaycon in https://github.com/netbirdio/netbird/pull/6688 * [client] Fix hanging status command during relay dial by @theodorsm in https://github.com/netbirdio/netbird/pull/6694 --------- Co-authored-by: Theodor Midtlien Co-authored-by: blaugrau90 <61945343+blaugrau90@users.noreply.github.com> Co-authored-by: Viktor Liu <17948409+lixmal@users.noreply.github.com> --- client/internal/engine.go | 2 +- client/internal/peer/conn.go | 42 ++- client/internal/peer/conn_test.go | 82 ++++++ client/internal/peer/wg_watcher.go | 14 +- client/internal/peer/wg_watcher_test.go | 72 ++++- client/internal/rosenpass/manager.go | 11 +- client/internal/rosenpass/manager_test.go | 26 +- client/internal/rosenpass/netbird_handler.go | 166 +++++++++--- .../rosenpass/netbird_handler_test.go | 250 ++++++++++++++++++ client/internal/rosenpass/seed.go | 17 ++ go.mod | 18 +- go.sum | 32 +-- management/internals/server/server.go | 3 +- management/server/types/account.go | 3 +- signal/cmd/run.go | 3 +- 15 files changed, 649 insertions(+), 92 deletions(-) create mode 100644 client/internal/rosenpass/netbird_handler_test.go diff --git a/client/internal/engine.go b/client/internal/engine.go index 4367f68b0..7b2fc7b26 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -551,7 +551,7 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL) } else { log.Infof("running rosenpass in strict mode") } - e.rpManager, err = rosenpass.NewManager(e.config.PreSharedKey, e.config.WgIfaceName) + e.rpManager, err = rosenpass.NewManager(e.config.PreSharedKey, e.config.WgIfaceName, publicKey) if err != nil { return fmt.Errorf("create rosenpass manager: %w", err) } diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index fb468696f..f0625c853 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -30,6 +30,11 @@ import ( relayClient "github.com/netbirdio/netbird/shared/relay/client" ) +// wgTimeoutEscalationThreshold is the number of consecutive WireGuard +// handshake timeouts after which the rosenpass state for the peer is +// considered desynced and gets reset. +const wgTimeoutEscalationThreshold = 3 + // MetricsRecorder is an interface for recording peer connection metrics type MetricsRecorder interface { RecordConnectionStages( @@ -118,6 +123,9 @@ type Conn struct { wgWatcher *WGWatcher wgWatcherWg sync.WaitGroup wgWatcherCancel context.CancelFunc + // wgTimeouts counts consecutive WireGuard handshake timeouts without a + // successful handshake in between. Guarded by mu. + wgTimeouts int // used to store the remote Rosenpass key for Relayed connection in case of connection update from ice rosenpassRemoteKey []byte @@ -683,6 +691,29 @@ func (conn *Conn) onWGDisconnected() { default: conn.Log.Debugf("No active connection to close on WG timeout") } + + conn.escalateWGTimeoutLocked() +} + +// escalateWGTimeoutLocked resets the peer's rosenpass state after repeated +// handshake timeouts. With rosenpass enabled, persistent timeouts mean the +// preshared keys have desynced; the renewal exchange runs over the dead +// tunnel and cannot resync them. Reporting the peer disconnected drops its +// rosenpass state, so the next connection configuration programs the +// rendezvous key and the tunnel can bootstrap again. Callers must hold mu. +func (conn *Conn) escalateWGTimeoutLocked() { + if conn.config.RosenpassConfig.PubKey == nil { + return + } + + conn.wgTimeouts++ + if conn.wgTimeouts < wgTimeoutEscalationThreshold || conn.onDisconnected == nil { + return + } + conn.wgTimeouts = 0 + + conn.Log.Warnf("%d consecutive WireGuard handshake timeouts, resetting rosenpass state for peer", wgTimeoutEscalationThreshold) + conn.onDisconnected(conn.config.WgConfig.RemoteKey) } func (conn *Conn) updateRelayStatus(relayServerAddr string, rosenpassPubKey []byte, updateTime time.Time) { @@ -812,7 +843,7 @@ func (conn *Conn) enableWgWatcherIfNeeded(enabledTime time.Time) { conn.wgWatcherWg.Add(1) go func() { defer conn.wgWatcherWg.Done() - conn.wgWatcher.EnableWgWatcher(wgWatcherCtx, enabledTime, conn.onWGDisconnected, conn.onWGHandshakeSuccess) + conn.wgWatcher.EnableWgWatcher(wgWatcherCtx, enabledTime, conn.onWGDisconnected, conn.onWGHandshakeSuccess, conn.onWGCheckSuccess) }() } @@ -892,6 +923,15 @@ func (conn *Conn) onWGHandshakeSuccess(when time.Time) { conn.recordConnectionMetrics() } +// onWGCheckSuccess is called for every watcher check that observed a fresh +// handshake, including handshakes of connections that were already up when +// the watcher started. +func (conn *Conn) onWGCheckSuccess() { + conn.mu.Lock() + conn.wgTimeouts = 0 + conn.mu.Unlock() +} + // recordConnectionMetrics records connection stage timestamps as metrics func (conn *Conn) recordConnectionMetrics() { if conn.metricsRecorder == nil { diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go index 59216b647..f2312a66a 100644 --- a/client/internal/peer/conn_test.go +++ b/client/internal/peer/conn_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/netbirdio/netbird/client/iface" @@ -304,3 +305,84 @@ func TestConn_presharedKey_RosenpassManaged(t *testing.T) { t.Fatalf("expected non-nil presharedKey before Rosenpass manages PSK") } } + +func newWGTimeoutTestConn(rosenpassEnabled bool, disconnected *[]string) *Conn { + cfg := ConnConfig{ + Key: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=", + LocalKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=", + WgConfig: WgConfig{RemoteKey: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU="}, + } + if rosenpassEnabled { + cfg.RosenpassConfig = RosenpassConfig{PubKey: []byte("dummykey")} + } + + conn := &Conn{ + ctx: context.Background(), + config: cfg, + Log: log.WithField("peer", cfg.Key), + metricsStages: &MetricsStages{}, + } + conn.SetOnDisconnected(func(remotePeer string) { + *disconnected = append(*disconnected, remotePeer) + }) + return conn +} + +// TestConn_onWGDisconnected_EscalatesToRosenpassReset: repeated handshake +// timeouts with rosenpass enabled mean the preshared keys have desynced. The +// renewal exchange runs over the dead tunnel and cannot resync them, so after +// wgTimeoutEscalationThreshold consecutive timeouts the conn must report the +// peer disconnected, dropping its rosenpass state so the next configuration +// programs the rendezvous key. +func TestConn_onWGDisconnected_EscalatesToRosenpassReset(t *testing.T) { + var disconnected []string + conn := newWGTimeoutTestConn(true, &disconnected) + + for i := 0; i < wgTimeoutEscalationThreshold-1; i++ { + conn.onWGDisconnected() + } + assert.Empty(t, disconnected, "escalation must not fire below the threshold") + + conn.onWGDisconnected() + assert.Equal(t, []string{conn.config.WgConfig.RemoteKey}, disconnected, + "reaching the threshold must report the peer disconnected once") + + for i := 0; i < wgTimeoutEscalationThreshold-1; i++ { + conn.onWGDisconnected() + } + assert.Len(t, disconnected, 1, "escalation must restart counting after firing") + + conn.onWGDisconnected() + assert.Len(t, disconnected, 2, "continued timeouts must escalate again") +} + +// TestConn_onWGDisconnected_CheckSuccessResetsEscalation: a successful +// handshake between timeouts means the tunnel recovered; the counter must +// start over. +func TestConn_onWGDisconnected_CheckSuccessResetsEscalation(t *testing.T) { + var disconnected []string + conn := newWGTimeoutTestConn(true, &disconnected) + + for i := 0; i < wgTimeoutEscalationThreshold-1; i++ { + conn.onWGDisconnected() + } + conn.onWGCheckSuccess() + + for i := 0; i < wgTimeoutEscalationThreshold-1; i++ { + conn.onWGDisconnected() + } + assert.Empty(t, disconnected, "handshake success must reset the timeout count") +} + +// TestConn_onWGDisconnected_NoEscalationWithoutRosenpass: without rosenpass +// there is no per-peer key state to reset; repeated timeouts must not report +// disconnects. +func TestConn_onWGDisconnected_NoEscalationWithoutRosenpass(t *testing.T) { + var disconnected []string + conn := newWGTimeoutTestConn(false, &disconnected) + + for i := 0; i < wgTimeoutEscalationThreshold*3; i++ { + conn.onWGDisconnected() + } + assert.Empty(t, disconnected, "escalation must be limited to rosenpass connections") +} diff --git a/client/internal/peer/wg_watcher.go b/client/internal/peer/wg_watcher.go index 4fc883d17..10c22153f 100644 --- a/client/internal/peer/wg_watcher.go +++ b/client/internal/peer/wg_watcher.go @@ -71,9 +71,11 @@ func (w *WGWatcher) PrepareInitialHandshake() (ok bool) { // EnableWgWatcher runs the WireGuard watcher loop using the handshake baseline captured by // PrepareInitialHandshake. The watcher runs until ctx is cancelled. Caller is responsible -// for context lifecycle management. -func (w *WGWatcher) EnableWgWatcher(ctx context.Context, enabledTime time.Time, onDisconnectedFn func(), onHandshakeSuccessFn func(when time.Time)) { - w.periodicHandshakeCheck(ctx, onDisconnectedFn, onHandshakeSuccessFn, enabledTime, w.initialHandshake) +// for context lifecycle management. onHandshakeSuccessFn is called only for the first +// handshake observed by this run, onCheckSuccessFn for every check that observed a fresh +// handshake, including the first. +func (w *WGWatcher) EnableWgWatcher(ctx context.Context, enabledTime time.Time, onDisconnectedFn func(), onHandshakeSuccessFn func(when time.Time), onCheckSuccessFn func()) { + w.periodicHandshakeCheck(ctx, onDisconnectedFn, onHandshakeSuccessFn, onCheckSuccessFn, enabledTime, w.initialHandshake) w.muEnabled.Lock() w.enabled = false @@ -90,7 +92,7 @@ func (w *WGWatcher) Reset() { } // wgStateCheck help to check the state of the WireGuard handshake and relay connection -func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, onDisconnectedFn func(), onHandshakeSuccessFn func(when time.Time), enabledTime time.Time, initialHandshake time.Time) { +func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, onDisconnectedFn func(), onHandshakeSuccessFn func(when time.Time), onCheckSuccessFn func(), enabledTime time.Time, initialHandshake time.Time) { w.log.Infof("WireGuard watcher started") timer := time.NewTimer(wgHandshakeOvertime) @@ -117,6 +119,10 @@ func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, onDisconnectedFn } } + if onCheckSuccessFn != nil && ctx.Err() == nil { + onCheckSuccessFn() + } + lastHandshake = *handshake resetTime := time.Until(handshake.Add(checkPeriod)) diff --git a/client/internal/peer/wg_watcher_test.go b/client/internal/peer/wg_watcher_test.go index 634d7974f..80f34f1a1 100644 --- a/client/internal/peer/wg_watcher_test.go +++ b/client/internal/peer/wg_watcher_test.go @@ -24,6 +24,72 @@ func (m *MocWgIface) disconnect() { m.stop = true } +type mockHandshakeStats struct { + mu sync.Mutex + handshake time.Time +} + +func (m *mockHandshakeStats) GetStats() (map[string]configurer.WGStats, error) { + m.mu.Lock() + defer m.mu.Unlock() + return map[string]configurer.WGStats{"": {LastHandshake: m.handshake}}, nil +} + +func (m *mockHandshakeStats) advance() { + m.mu.Lock() + defer m.mu.Unlock() + m.handshake = time.Now() +} + +// TestWGWatcher_CheckSuccessCallback: onCheckSuccessFn must fire for a fresh +// handshake even when the watcher started with an existing handshake baseline, +// the case where onHandshakeSuccessFn stays silent. +func TestWGWatcher_CheckSuccessCallback(t *testing.T) { + // checkPeriod bounds how stale a handshake may be before the watcher treats it + // as a suspended-machine timeout. The first check fires after wgHandshakeOvertime, + // so keep checkPeriod well above any scheduling jitter to avoid a false timeout + // converting the expected success into a disconnect on a loaded runner. + checkPeriod = 1 * time.Minute + wgHandshakeOvertime = 1 * time.Second + + mlog := log.WithField("peer", "tet") + // Use an old baseline so advance() yields a strictly newer handshake even on + // platforms with coarse clock resolution (Windows), where two time.Now() calls + // microseconds apart can return the same instant and read as a timed-out handshake. + stats := &mockHandshakeStats{handshake: time.Now().Add(-time.Hour)} + watcher := NewWGWatcher(mlog, stats, "", newStateDump("peer", mlog, &Status{})) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + require.True(t, watcher.PrepareInitialHandshake()) + + firstHandshake := make(chan struct{}, 1) + checkSuccess := make(chan struct{}, 1) + go watcher.EnableWgWatcher(ctx, time.Now(), func() {}, func(when time.Time) { + firstHandshake <- struct{}{} + }, func() { + select { + case checkSuccess <- struct{}{}: + default: + } + }) + + stats.advance() + + select { + case <-checkSuccess: + case <-time.After(10 * time.Second): + t.Errorf("timeout waiting for check success callback") + } + + select { + case <-firstHandshake: + t.Errorf("first-handshake callback must not fire for a non-zero baseline") + default: + } +} + func TestWGWatcher_EnableWgWatcher(t *testing.T) { checkPeriod = 5 * time.Second wgHandshakeOvertime = 1 * time.Second @@ -44,7 +110,7 @@ func TestWGWatcher_EnableWgWatcher(t *testing.T) { onDisconnected <- struct{}{} }, func(when time.Time) { mlog.Infof("onHandshakeSuccess: %v", when) - }) + }, nil) // wait for initial reading time.Sleep(2 * time.Second) @@ -73,7 +139,7 @@ func TestWGWatcher_ReEnable(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - watcher.EnableWgWatcher(ctx, time.Now(), func() {}, func(when time.Time) {}) + watcher.EnableWgWatcher(ctx, time.Now(), func() {}, func(when time.Time) {}, nil) }() cancel() @@ -89,7 +155,7 @@ func TestWGWatcher_ReEnable(t *testing.T) { onDisconnected := make(chan struct{}, 1) go watcher.EnableWgWatcher(ctx, time.Now(), func() { onDisconnected <- struct{}{} - }, func(when time.Time) {}) + }, func(when time.Time) {}, nil) time.Sleep(2 * time.Second) mocWgIface.disconnect() diff --git a/client/internal/rosenpass/manager.go b/client/internal/rosenpass/manager.go index 903753753..21dd751df 100644 --- a/client/internal/rosenpass/manager.go +++ b/client/internal/rosenpass/manager.go @@ -39,6 +39,7 @@ type rpServer interface { type Manager struct { ifaceName string + localWgKey wgtypes.Key spk []byte ssk []byte rpKeyHash string @@ -51,8 +52,9 @@ type Manager struct { wgIface PresharedKeySetter } -// NewManager creates a new Rosenpass manager -func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string) (*Manager, error) { +// NewManager creates a new Rosenpass manager. localWgKey is the local +// WireGuard public key, used to derive the per-peer rendezvous key. +func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string, localWgKey wgtypes.Key) (*Manager, error) { public, secret, err := rp.GenerateKeyPair() if err != nil { return nil, err @@ -62,6 +64,7 @@ func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string) (*Manager, error) log.Tracef("generated new rosenpass key pair with public key %s", rpKeyHash) return &Manager{ ifaceName: wgIfaceName, + localWgKey: localWgKey, rpKeyHash: rpKeyHash, spk: public, ssk: secret, @@ -73,7 +76,7 @@ func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string) (*Manager, error) // nil receiver in addPeer -> m.rpWgHandler.AddPeer. generateConfig will // replace it with a fresh handler on each Run() to clear stale peer // state from previous engine sessions. - rpWgHandler: NewNetbirdHandler(), + rpWgHandler: NewNetbirdHandler((*[32]byte)(preSharedKey), localWgKey), lock: sync.Mutex{}, }, nil } @@ -161,7 +164,7 @@ func (m *Manager) generateConfig() (rp.Config, error) { cfg.Peers = []rp.PeerConfig{} m.lock.Lock() - m.rpWgHandler = NewNetbirdHandler() + m.rpWgHandler = NewNetbirdHandler(m.preSharedKey, m.localWgKey) if m.wgIface != nil { m.rpWgHandler.SetInterface(m.wgIface) } diff --git a/client/internal/rosenpass/manager_test.go b/client/internal/rosenpass/manager_test.go index d74960d0d..69e18ac88 100644 --- a/client/internal/rosenpass/manager_test.go +++ b/client/internal/rosenpass/manager_test.go @@ -85,7 +85,7 @@ func newTestManager(spkFirstByte byte, mock *mockServer) *Manager { ssk: make([]byte, 32), rpKeyHash: "test-hash", rpPeerIDs: make(map[string]*rp.PeerID), - rpWgHandler: NewNetbirdHandler(), + rpWgHandler: NewNetbirdHandler(nil, wgtypes.Key{0x01}), server: mock, } } @@ -255,7 +255,7 @@ func TestAddPeer_NilServer_ReturnsErrorNoCrash(t *testing.T) { // issue #4341 cannot occur in the window between NewManager and Run(). func TestNewManager_PreInitializesHandler(t *testing.T) { psk := wgtypes.Key{} - m, err := NewManager(&psk, "wt0") + m, err := NewManager(&psk, "wt0", wgtypes.Key{0x01}) require.NoError(t, err) require.NotNil(t, m.rpWgHandler, "rpWgHandler must be initialized in NewManager") } @@ -329,10 +329,10 @@ func TestIsPresharedKeyInitialized_AddedButNotHandshaken_ReturnsFalse(t *testing require.False(t, m.IsPresharedKeyInitialized(wgKey)) } -// --- NetbirdHandler.outputKey ---------------------------------------------- +// --- NetbirdHandler.applyKey ---------------------------------------------- -func TestHandler_OutputKey_FirstCallUsesUpdateOnlyFalse(t *testing.T) { - h := NewNetbirdHandler() +func TestHandler_ApplyKey_FirstCallUsesUpdateOnlyFalse(t *testing.T) { + h := NewNetbirdHandler(nil, wgtypes.Key{0x01}) iface := &mockIface{} h.SetInterface(iface) @@ -348,8 +348,8 @@ func TestHandler_OutputKey_FirstCallUsesUpdateOnlyFalse(t *testing.T) { require.Equal(t, wgKey.String(), iface.calls[0].peerKey) } -func TestHandler_OutputKey_SubsequentCallsUseUpdateOnlyTrue(t *testing.T) { - h := NewNetbirdHandler() +func TestHandler_ApplyKey_SubsequentCallsUseUpdateOnlyTrue(t *testing.T) { + h := NewNetbirdHandler(nil, wgtypes.Key{0x01}) iface := &mockIface{} h.SetInterface(iface) @@ -364,8 +364,8 @@ func TestHandler_OutputKey_SubsequentCallsUseUpdateOnlyTrue(t *testing.T) { require.True(t, iface.calls[1].updateOnly, "subsequent rotations must use updateOnly=true") } -func TestHandler_OutputKey_NilInterface_NoCrashNoCall(t *testing.T) { - h := NewNetbirdHandler() +func TestHandler_ApplyKey_NilInterface_NoCrashNoCall(t *testing.T) { + h := NewNetbirdHandler(nil, wgtypes.Key{0x01}) // no SetInterface — iface remains nil pid := rp.PeerID{0x03} h.AddPeer(pid, "wt0", rp.Key(wgtypes.Key{})) @@ -374,8 +374,8 @@ func TestHandler_OutputKey_NilInterface_NoCrashNoCall(t *testing.T) { h.HandshakeCompleted(pid, rp.Key{}) } -func TestHandler_OutputKey_UnknownPeer_NoCall(t *testing.T) { - h := NewNetbirdHandler() +func TestHandler_ApplyKey_UnknownPeer_NoCall(t *testing.T) { + h := NewNetbirdHandler(nil, wgtypes.Key{0x01}) iface := &mockIface{} h.SetInterface(iface) @@ -384,7 +384,7 @@ func TestHandler_OutputKey_UnknownPeer_NoCall(t *testing.T) { } func TestHandler_RemovePeer_ClearsInitializedState(t *testing.T) { - h := NewNetbirdHandler() + h := NewNetbirdHandler(nil, wgtypes.Key{0x01}) iface := &mockIface{} h.SetInterface(iface) @@ -398,7 +398,7 @@ func TestHandler_RemovePeer_ClearsInitializedState(t *testing.T) { } func TestHandler_SetInterfaceAfterAddPeer_StillReceivesKey(t *testing.T) { - h := NewNetbirdHandler() + h := NewNetbirdHandler(nil, wgtypes.Key{0x01}) pid := rp.PeerID{0x05} wgKey := wgtypes.Key{0xEE} h.AddPeer(pid, "wt0", rp.Key(wgKey)) diff --git a/client/internal/rosenpass/netbird_handler.go b/client/internal/rosenpass/netbird_handler.go index 9de2409ef..672650ca7 100644 --- a/client/internal/rosenpass/netbird_handler.go +++ b/client/internal/rosenpass/netbird_handler.go @@ -18,19 +18,34 @@ type PresharedKeySetter interface { type wireGuardPeer struct { Interface string PublicKey rp.Key + // initialized is true once a completed exchange has set a + // Rosenpass-managed PSK for this peer. + initialized bool + // chainKey is the key output by the last completed exchange, advanced by + // one ratchet step on expiry. Nil until the first exchange completes and + // after the peer has fallen back to the rendezvous key. + chainKey *wgtypes.Key + // expiries counts failed renewals since the last completed exchange. + expiries int } type NetbirdHandler struct { - mu sync.Mutex - iface PresharedKeySetter - peers map[rp.PeerID]wireGuardPeer - initializedPeers map[rp.PeerID]bool + mu sync.Mutex + iface PresharedKeySetter + // preSharedKey is the account-level preshared key, used as the rendezvous + // key when set. Nil means the deterministic seed key is used instead. + preSharedKey *[32]byte + // localWgKey is the local WireGuard public key, one of the two inputs to + // the deterministic seed key. + localWgKey wgtypes.Key + peers map[rp.PeerID]*wireGuardPeer } -func NewNetbirdHandler() *NetbirdHandler { +func NewNetbirdHandler(preSharedKey *[32]byte, localWgKey wgtypes.Key) *NetbirdHandler { return &NetbirdHandler{ - peers: map[rp.PeerID]wireGuardPeer{}, - initializedPeers: map[rp.PeerID]bool{}, + preSharedKey: preSharedKey, + localWgKey: localWgKey, + peers: map[rp.PeerID]*wireGuardPeer{}, } } @@ -42,10 +57,16 @@ func (h *NetbirdHandler) SetInterface(iface PresharedKeySetter) { h.iface = iface } +// AddPeer registers a peer with the handler. Re-adding a known peer (every +// reconnection does) keeps its key recovery state. func (h *NetbirdHandler) AddPeer(pid rp.PeerID, intf string, pk rp.Key) { h.mu.Lock() defer h.mu.Unlock() - h.peers[pid] = wireGuardPeer{ + if existing, ok := h.peers[pid]; ok && existing.PublicKey == pk { + existing.Interface = intf + return + } + h.peers[pid] = &wireGuardPeer{ Interface: intf, PublicKey: pk, } @@ -55,7 +76,6 @@ 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 @@ -63,50 +83,120 @@ func (h *NetbirdHandler) RemovePeer(pid rp.PeerID) { func (h *NetbirdHandler) IsPeerInitialized(pid rp.PeerID) bool { h.mu.Lock() defer h.mu.Unlock() - return h.initializedPeers[pid] + peer, ok := h.peers[pid] + return ok && peer.initialized } +// HandshakeCompleted programs the freshly exchanged output key and resets the +// peer's key recovery state. func (h *NetbirdHandler) HandshakeCompleted(pid rp.PeerID, key rp.Key) { - h.outputKey(rp.KeyOutputReasonStale, pid, key) -} + psk := wgtypes.Key(key) -func (h *NetbirdHandler) HandshakeExpired(pid rp.PeerID) { - key, _ := rp.GeneratePresharedKey() - 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() + defer h.mu.Unlock() - if iface == nil { - log.Warn("rosenpass: interface not set, cannot update preshared key") + peer, ok := h.peers[pid] + if !ok { return } + if peer.expiries > 0 { + log.Infof("rosenpass exchange completed for peer %s after %d expired renewals", wgtypes.Key(peer.PublicKey), peer.expiries) + } + // chainKey tracks the shared exchange output regardless of the local write + // outcome, so both ends still converge on the next expiry. + peer.chainKey = &psk + peer.expiries = 0 + if !h.applyKeyLocked(pid, psk, peer.initialized) { + return + } + peer.initialized = true +} +// HandshakeExpired replaces the expired key. The renewal exchange runs over +// the tunnel keyed by the PSK itself, so the replacement must be derivable on +// both ends without communication: the first expiry ratchets the last shared +// key forward, repeated expiries (and expiries without a completed exchange) +// fall back to the rendezvous key and drop the peer out of the initialized +// state so connection reconfigurations reprogram the rendezvous key as well. +func (h *NetbirdHandler) HandshakeExpired(pid rp.PeerID) { + h.mu.Lock() + defer h.mu.Unlock() + + peer, ok := h.peers[pid] if !ok { return } - peerKey := wgtypes.Key(wg.PublicKey).String() - pskKey := wgtypes.Key(psk) + peer.expiries++ - // 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 { + var psk wgtypes.Key + if peer.chainKey != nil && peer.expiries == 1 { + log.Infof("rosenpass key for peer %s expired without renewal, advancing to ratcheted key", wgtypes.Key(peer.PublicKey)) + psk = RatchetKey(*peer.chainKey) + peer.chainKey = &psk + } else { + rendezvous, err := h.rendezvousKey(peer) + if err != nil { + // Fail closed: without a rendezvous key the expired key must + // still be rotated out, even if the replacement is unusable. + log.Errorf("failed to derive rendezvous key, replacing expired key with a random one: %v", err) + h.applyRandomKeyLocked(pid) + return + } + log.Warnf("rosenpass key for peer %s expired %d times without renewal, falling back to the rendezvous key", wgtypes.Key(peer.PublicKey), peer.expiries) + psk = rendezvous + peer.chainKey = nil + peer.initialized = false + } + + h.applyKeyLocked(pid, psk, true) +} + +// rendezvousKey returns the key both ends converge on without communication: +// the account-level preshared key when configured, the deterministic seed key +// otherwise. It mirrors the key that peer connections program when Rosenpass +// does not manage the peer yet. +func (h *NetbirdHandler) rendezvousKey(peer *wireGuardPeer) (wgtypes.Key, error) { + if h.preSharedKey != nil { + return *h.preSharedKey, nil + } + + seed, err := DeterministicSeedKey(h.localWgKey.String(), wgtypes.Key(peer.PublicKey).String()) + if err != nil { + return wgtypes.Key{}, err + } + return *seed, nil +} + +// applyKeyLocked writes the preshared key for the peer to the WireGuard +// interface and reports whether the write succeeded. Callers must hold h.mu +// for the whole state-mutation-plus-write so that a concurrent completion and +// expiry cannot reorder their writes relative to the in-memory chain key. +func (h *NetbirdHandler) applyKeyLocked(pid rp.PeerID, psk wgtypes.Key, updateOnly bool) bool { + peer, ok := h.peers[pid] + if !ok { + return false + } + + if h.iface == nil { + log.Warn("rosenpass: interface not set, cannot update preshared key") + return false + } + + peerKey := wgtypes.Key(peer.PublicKey).String() + if err := h.iface.SetPresharedKey(peerKey, psk, updateOnly); err != nil { log.Errorf("Failed to apply rosenpass key: %v", err) + return false + } + + return true +} + +func (h *NetbirdHandler) applyRandomKeyLocked(pid rp.PeerID) { + key, err := rp.GeneratePresharedKey() + if err != nil { + log.Errorf("failed to generate random preshared key: %v", err) 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 - } - h.mu.Unlock() - } + h.applyKeyLocked(pid, wgtypes.Key(key), true) } diff --git a/client/internal/rosenpass/netbird_handler_test.go b/client/internal/rosenpass/netbird_handler_test.go new file mode 100644 index 000000000..9d91ba93b --- /dev/null +++ b/client/internal/rosenpass/netbird_handler_test.go @@ -0,0 +1,250 @@ +package rosenpass + +import ( + "testing" + + rp "cunicu.li/go-rosenpass" + "github.com/stretchr/testify/require" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +// handlerTestLink wires two NetbirdHandlers as the two ends of a single +// tunnel: handler A manages the rosenpass peer B and vice versa, the way two +// NetBird clients see each other. +type handlerTestLink struct { + handlerA, handlerB *NetbirdHandler + ifaceA, ifaceB *mockIface + pidA, pidB rp.PeerID + wgKeyA, wgKeyB wgtypes.Key +} + +func newHandlerTestLink(t *testing.T, preSharedKey *[32]byte) *handlerTestLink { + t.Helper() + + link := &handlerTestLink{ + ifaceA: &mockIface{}, + ifaceB: &mockIface{}, + } + link.pidA[0] = 0xaa + link.pidB[0] = 0xbb + link.wgKeyA[31] = 1 + link.wgKeyB[31] = 2 + + link.handlerA = NewNetbirdHandler(preSharedKey, link.wgKeyA) + link.handlerB = NewNetbirdHandler(preSharedKey, link.wgKeyB) + + link.handlerA.SetInterface(link.ifaceA) + link.handlerB.SetInterface(link.ifaceB) + + link.handlerA.AddPeer(link.pidB, "wt0", rp.Key(link.wgKeyB)) + link.handlerB.AddPeer(link.pidA, "wt0", rp.Key(link.wgKeyA)) + + return link +} + +// complete simulates a completed rosenpass exchange: both ends derive the +// same output key. +func (l *handlerTestLink) complete(osk rp.Key) { + l.handlerA.HandshakeCompleted(l.pidB, osk) + l.handlerB.HandshakeCompleted(l.pidA, osk) +} + +// expire simulates a failed key renewal on both ends. +func (l *handlerTestLink) expire() { + l.handlerA.HandshakeExpired(l.pidB) + l.handlerB.HandshakeExpired(l.pidA) +} + +func lastPSK(t *testing.T, m *mockIface) wgtypes.Key { + t.Helper() + m.mu.Lock() + defer m.mu.Unlock() + require.NotEmpty(t, m.calls, "expected at least one SetPresharedKey call") + return m.calls[len(m.calls)-1].psk +} + +func TestHandshakeCompleted_SetsKeyAndInitializes(t *testing.T) { + link := newHandlerTestLink(t, nil) + + var osk rp.Key + osk[0] = 0x42 + link.complete(osk) + + require.Equal(t, wgtypes.Key(osk), lastPSK(t, link.ifaceA), "completed exchange must program the osk") + require.False(t, link.ifaceA.calls[0].updateOnly, "first rotation must not be update-only") + require.True(t, link.handlerA.IsPeerInitialized(link.pidB), "peer must be initialized after first completed exchange") + + link.complete(osk) + require.True(t, link.ifaceA.calls[1].updateOnly, "later rotations must be update-only") +} + +// TestHandshakeExpired_BothSidesConverge encodes the core recovery invariant: +// rosenpass renewals run over the tunnel that the PSK itself keys, so when a +// renewal fails on both ends, both ends must fall back to the same key or the +// tunnel can never handshake again. +func TestHandshakeExpired_BothSidesConverge(t *testing.T) { + link := newHandlerTestLink(t, nil) + + var osk rp.Key + osk[0] = 0x42 + link.complete(osk) + + link.expire() + keyA := lastPSK(t, link.ifaceA) + keyB := lastPSK(t, link.ifaceB) + require.NotEqual(t, wgtypes.Key(osk), keyA, "expired key must be rotated out") + require.Equal(t, keyA, keyB, "both ends must converge on the same key after expiry") + + link.expire() + require.Equal(t, lastPSK(t, link.ifaceA), lastPSK(t, link.ifaceB), + "both ends must still converge after repeated expiries") +} + +// TestHandshakeExpired_ExpiryWithoutCompletionConverges covers the bootstrap +// case: the initial exchange never completed (the tunnel ran on the rendezvous +// key), so an expiry must not replace the working key with an unrecoverable +// one on either end. +func TestHandshakeExpired_ExpiryWithoutCompletionConverges(t *testing.T) { + link := newHandlerTestLink(t, nil) + + link.expire() + require.Equal(t, lastPSK(t, link.ifaceA), lastPSK(t, link.ifaceB), + "both ends must converge when the exchange never completed") +} + +// TestHandshakeExpired_RepeatedExpiryClearsInitialized: once renewals keep +// failing, the peer must drop out of the initialized state so the next +// connection reconfiguration reprograms the rendezvous key instead of +// preserving a poisoned rosenpass-managed key. +func TestHandshakeExpired_RepeatedExpiryClearsInitialized(t *testing.T) { + link := newHandlerTestLink(t, nil) + + var osk rp.Key + osk[0] = 0x42 + link.complete(osk) + + link.expire() + link.expire() + + require.False(t, link.handlerA.IsPeerInitialized(link.pidB), + "repeated expiries must clear the initialized state") + require.False(t, link.handlerB.IsPeerInitialized(link.pidA), + "repeated expiries must clear the initialized state") +} + +// TestHandshakeCompleted_AfterExpiryRecovers: a completed exchange after a +// desync must fully reset the recovery state. +func TestHandshakeCompleted_AfterExpiryRecovers(t *testing.T) { + link := newHandlerTestLink(t, nil) + + var osk1, osk2 rp.Key + osk1[0] = 1 + osk2[0] = 2 + + link.complete(osk1) + link.expire() + link.expire() + + link.complete(osk2) + require.Equal(t, wgtypes.Key(osk2), lastPSK(t, link.ifaceA), "new exchange must program the fresh osk") + require.True(t, link.handlerA.IsPeerInitialized(link.pidB), "peer must be initialized again after recovery") + + link.expire() + require.Equal(t, lastPSK(t, link.ifaceA), lastPSK(t, link.ifaceB), + "recovered link must converge again on the next expiry") + require.NotEqual(t, wgtypes.Key(osk2), lastPSK(t, link.ifaceA), "expired key must be rotated out") +} + +// TestHandshakeExpired_FirstExpiryRatchetsLastKey: the first expiry must +// derive the replacement from the last shared key, so an attacker who only +// blocks the renewal exchange gains nothing over the previous key. +func TestHandshakeExpired_FirstExpiryRatchetsLastKey(t *testing.T) { + link := newHandlerTestLink(t, nil) + + var osk rp.Key + osk[0] = 0x42 + link.complete(osk) + + link.expire() + require.Equal(t, RatchetKey(wgtypes.Key(osk)), lastPSK(t, link.ifaceA), + "first expiry must program the ratcheted key") + require.True(t, link.handlerA.IsPeerInitialized(link.pidB), + "ratchet step must keep the peer initialized so reconfigurations preserve the key") +} + +// TestHandshakeExpired_RepeatedExpiryFallsBackToSeed: once the ratchet key +// also fails, both ends must land on the same key that peer connections +// program for uninitialized peers, so a reconnect completes the recovery. +func TestHandshakeExpired_RepeatedExpiryFallsBackToSeed(t *testing.T) { + link := newHandlerTestLink(t, nil) + + var osk rp.Key + osk[0] = 0x42 + link.complete(osk) + + link.expire() + link.expire() + + seed, err := DeterministicSeedKey(link.wgKeyA.String(), link.wgKeyB.String()) + require.NoError(t, err) + require.Equal(t, *seed, lastPSK(t, link.ifaceA), "repeated expiry must fall back to the seed key") + require.Equal(t, *seed, lastPSK(t, link.ifaceB), "repeated expiry must fall back to the seed key") +} + +// TestHandshakeExpired_ConfiguredPSKUsedAsRendezvous: with an account-level +// preshared key configured, the fallback must be that key, matching what peer +// connections program for uninitialized peers. +func TestHandshakeExpired_ConfiguredPSKUsedAsRendezvous(t *testing.T) { + psk := &[32]byte{0x77} + link := newHandlerTestLink(t, psk) + + var osk rp.Key + osk[0] = 0x42 + link.complete(osk) + + link.expire() + link.expire() + + require.Equal(t, wgtypes.Key(*psk), lastPSK(t, link.ifaceA), + "fallback must be the configured preshared key") + require.Equal(t, wgtypes.Key(*psk), lastPSK(t, link.ifaceB), + "fallback must be the configured preshared key on both ends") +} + +// TestHandshakeExpired_ExpiryWritesAreUpdateOnly: expiry replacements must +// never create a WireGuard peer that connection management has removed. +func TestHandshakeExpired_ExpiryWritesAreUpdateOnly(t *testing.T) { + link := newHandlerTestLink(t, nil) + + var osk rp.Key + osk[0] = 0x42 + link.complete(osk) + + link.expire() + link.expire() + + for _, call := range link.ifaceA.calls[1:] { + require.True(t, call.updateOnly, "expiry writes must be update-only") + } +} + +// TestAddPeer_ReAddKeepsRecoveryState: reconnections re-add the peer on every +// OnConnected; that must not reset the expiry chain state. +func TestAddPeer_ReAddKeepsRecoveryState(t *testing.T) { + link := newHandlerTestLink(t, nil) + + var osk rp.Key + osk[0] = 0x42 + link.complete(osk) + link.expire() + + link.handlerA.AddPeer(link.pidB, "wt0", rp.Key(link.wgKeyB)) + require.True(t, link.handlerA.IsPeerInitialized(link.pidB), + "re-adding a known peer must keep its state") + + link.expire() + seed, err := DeterministicSeedKey(link.wgKeyA.String(), link.wgKeyB.String()) + require.NoError(t, err) + require.Equal(t, *seed, lastPSK(t, link.ifaceA), + "second expiry after re-add must continue to the seed fallback") +} diff --git a/client/internal/rosenpass/seed.go b/client/internal/rosenpass/seed.go index 83aba1e0e..052c11ed4 100644 --- a/client/internal/rosenpass/seed.go +++ b/client/internal/rosenpass/seed.go @@ -1,11 +1,28 @@ package rosenpass import ( + "crypto/sha256" "fmt" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) +// ratchetLabel domain-separates the expiry ratchet from other uses of the +// rosenpass output key. +const ratchetLabel = "netbird-rosenpass-expiry-ratchet" + +// RatchetKey derives the successor preshared key from the previous Rosenpass +// output key. When a key expires without a completed renewal, both peers +// advance their last shared key by one ratchet step: the expired key is +// rotated out while both ends still converge on an identical, non-public +// replacement without communicating. +func RatchetKey(prev wgtypes.Key) wgtypes.Key { + input := make([]byte, 0, len(ratchetLabel)+len(prev)) + input = append(input, ratchetLabel...) + input = append(input, prev[:]...) + return sha256.Sum256(input) +} + // DeterministicSeedKey derives a 32-byte WireGuard preshared key from a pair // of peer public keys. Both peers, given the same key pair, produce the same // output regardless of which side runs the function: the inputs are ordered diff --git a/go.mod b/go.mod index dbbd3e35b..b90b68446 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/netbirdio/netbird go 1.25.5 -toolchain go1.25.11 +toolchain go1.25.12 require ( cunicu.li/go-rosenpass v0.5.42 @@ -19,8 +19,8 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/vishvananda/netlink v1.3.1 - golang.org/x/crypto v0.50.0 - golang.org/x/sys v0.43.0 + golang.org/x/crypto v0.54.0 + golang.org/x/sys v0.47.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 golang.zx2c4.com/wireguard/windows v0.5.3 @@ -126,11 +126,11 @@ require ( goauthentik.io/api/v3 v3.2023051.3 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f golang.org/x/mobile v0.0.0-20251113184115-a159579294ab - golang.org/x/mod v0.35.0 - golang.org/x/net v0.53.0 + golang.org/x/mod v0.37.0 + golang.org/x/net v0.56.0 golang.org/x/oauth2 v0.36.0 - golang.org/x/sync v0.20.0 - golang.org/x/term v0.42.0 + golang.org/x/sync v0.22.0 + golang.org/x/term v0.45.0 golang.org/x/time v0.15.0 google.golang.org/api v0.276.0 gopkg.in/yaml.v3 v3.0.1 @@ -314,8 +314,8 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/text v0.37.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/text v0.40.0 // indirect + golang.org/x/tools v0.47.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect diff --git a/go.sum b/go.sum index d4be37df0..ad43dc109 100644 --- a/go.sum +++ b/go.sum @@ -732,8 +732,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= @@ -748,8 +748,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -768,8 +768,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= @@ -784,8 +784,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -821,8 +821,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -835,8 +835,8 @@ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -848,8 +848,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -863,8 +863,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/management/internals/server/server.go b/management/internals/server/server.go index 9411073ac..7fd06d947 100644 --- a/management/internals/server/server.go +++ b/management/internals/server/server.go @@ -15,7 +15,7 @@ import ( "go.opentelemetry.io/otel/metric" "golang.org/x/crypto/acme/autocert" "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" + "golang.org/x/net/http2/h2c" //nolint:staticcheck "google.golang.org/grpc" "github.com/netbirdio/netbird/encryption" @@ -382,6 +382,7 @@ func (s *BaseServer) serveGRPCWithHTTP(ctx context.Context, listener net.Listene // the following magic is needed to support HTTP2 without TLS // and still share a single port between gRPC and HTTP APIs h1s := &http.Server{ + //nolint:staticcheck // h2c also handles the HTTP/1 Upgrade mechanism, which http.Server's UnencryptedHTTP2 does not Handler: h2c.NewHandler(handler, &http2.Server{}), } err = h1s.Serve(listener) diff --git a/management/server/types/account.go b/management/server/types/account.go index 7a0a0054f..6be865a43 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -305,7 +305,8 @@ func (a *Account) SynthesizePrivateServiceZones(peerID string) []nbdns.CustomZon zone = &nbdns.CustomZone{ Domain: dns.Fqdn(serviceDomainZone), Records: []nbdns.SimpleRecord{}, - NonAuthoritative: true, + NonAuthoritative: true, + SearchDomainDisabled: true, } zonesByApex[serviceDomainZone] = zone } diff --git a/signal/cmd/run.go b/signal/cmd/run.go index 681222403..81e9cc926 100644 --- a/signal/cmd/run.go +++ b/signal/cmd/run.go @@ -16,7 +16,7 @@ import ( "go.opentelemetry.io/otel/metric" "golang.org/x/crypto/acme/autocert" "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" + "golang.org/x/net/http2/h2c" //nolint:staticcheck "github.com/netbirdio/netbird/shared/metrics" @@ -281,6 +281,7 @@ func serveHTTP(httpListener net.Listener, handler http.Handler) { go func() { // Use h2c to support HTTP/2 without TLS (needed for gRPC) h1s := &http.Server{ + //nolint:staticcheck // h2c also handles the HTTP/1 Upgrade mechanism, which http.Server's UnencryptedHTTP2 does not Handler: h2c.NewHandler(handler, &http2.Server{}), } err := h1s.Serve(httpListener)