From ee3a67d2d8007dd380ad9e0f772f07863ab9ff7d Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 23 Jan 2026 17:06:07 +0100 Subject: [PATCH 1/4] [client] Fix/health result in bundle (#5164) * Add support for optional status refresh callback during debug bundle generation * Always update wg status * Remove duplicated wg status call --- client/internal/debug/debug.go | 7 +++++++ client/internal/engine.go | 24 ++++++------------------ client/internal/peer/status.go | 32 ++++++++++++++++++++++++++++++++ client/server/debug.go | 13 +++++++++++++ client/server/server.go | 4 ++++ 5 files changed, 62 insertions(+), 18 deletions(-) diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index 07a19036a..0f8243e7a 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -228,6 +228,7 @@ type BundleGenerator struct { syncResponse *mgmProto.SyncResponse logPath string cpuProfile []byte + refreshStatus func() // Optional callback to refresh status before bundle generation anonymize bool includeSystemInfo bool @@ -248,6 +249,7 @@ type GeneratorDependencies struct { SyncResponse *mgmProto.SyncResponse LogPath string CPUProfile []byte + RefreshStatus func() // Optional callback to refresh status before bundle generation } func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGenerator { @@ -265,6 +267,7 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen syncResponse: deps.SyncResponse, logPath: deps.LogPath, cpuProfile: deps.CPUProfile, + refreshStatus: deps.RefreshStatus, anonymize: cfg.Anonymize, includeSystemInfo: cfg.IncludeSystemInfo, @@ -408,6 +411,10 @@ func (g *BundleGenerator) addStatus() error { profName = activeProf.Name } + if g.refreshStatus != nil { + g.refreshStatus() + } + fullStatus := g.statusRecorder.GetFullStatus() protoFullStatus := nbstatus.ToProtoFullStatus(fullStatus) protoFullStatus.Events = g.statusRecorder.GetEventHistory() diff --git a/client/internal/engine.go b/client/internal/engine.go index 25a4e4048..a391ba22a 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1050,6 +1050,9 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR StatusRecorder: e.statusRecorder, SyncResponse: syncResponse, LogPath: e.config.LogPath, + RefreshStatus: func() { + e.RunHealthProbes(true) + }, } bundleJobParams := debug.BundleConfig{ @@ -1827,7 +1830,7 @@ func (e *Engine) getRosenpassAddr() string { return "" } -// RunHealthProbes executes health checks for Signal, Management, Relay and WireGuard services +// RunHealthProbes executes health checks for Signal, Management, Relay, and WireGuard services // and updates the status recorder with the latest states. func (e *Engine) RunHealthProbes(waitForResult bool) bool { e.syncMsgMux.Lock() @@ -1841,23 +1844,8 @@ func (e *Engine) RunHealthProbes(waitForResult bool) bool { stuns := slices.Clone(e.STUNs) turns := slices.Clone(e.TURNs) - if e.wgInterface != nil { - stats, err := e.wgInterface.GetStats() - if err != nil { - log.Warnf("failed to get wireguard stats: %v", err) - e.syncMsgMux.Unlock() - return false - } - for _, key := range e.peerStore.PeersPubKey() { - // wgStats could be zero value, in which case we just reset the stats - wgStats, ok := stats[key] - if !ok { - continue - } - if err := e.statusRecorder.UpdateWireGuardPeerState(key, wgStats); err != nil { - log.Debugf("failed to update wg stats for peer %s: %s", key, err) - } - } + if err := e.statusRecorder.RefreshWireGuardStats(); err != nil { + log.Debugf("failed to refresh WireGuard stats: %v", err) } e.syncMsgMux.Unlock() diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index 697bda2ff..abedc208e 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -1145,6 +1145,38 @@ func (d *Status) PeersStatus() (*configurer.Stats, error) { return d.wgIface.FullStats() } +// RefreshWireGuardStats fetches fresh WireGuard statistics from the interface +// and updates the cached peer states. This ensures accurate handshake times and +// transfer statistics in status reports without running full health probes. +func (d *Status) RefreshWireGuardStats() error { + d.mux.Lock() + defer d.mux.Unlock() + + if d.wgIface == nil { + return nil // silently skip if interface not set + } + + stats, err := d.wgIface.FullStats() + if err != nil { + return fmt.Errorf("get wireguard stats: %w", err) + } + + // Update each peer's WireGuard statistics + for _, peerStats := range stats.Peers { + peerState, ok := d.peers[peerStats.PublicKey] + if !ok { + continue + } + + peerState.LastWireguardHandshake = peerStats.LastHandshake + peerState.BytesRx = peerStats.RxBytes + peerState.BytesTx = peerStats.TxBytes + d.peers[peerStats.PublicKey] = peerState + } + + return nil +} + type EventQueue struct { maxSize int events []*proto.SystemEvent diff --git a/client/server/debug.go b/client/server/debug.go index 5646cea79..4c531efba 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -34,6 +34,18 @@ func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) ( }() } + // Prepare refresh callback for health probes + var refreshStatus func() + if s.connectClient != nil { + engine := s.connectClient.Engine() + if engine != nil { + refreshStatus = func() { + log.Debug("refreshing system health status for debug bundle") + engine.RunHealthProbes(true) + } + } + } + bundleGenerator := debug.NewBundleGenerator( debug.GeneratorDependencies{ InternalConfig: s.config, @@ -41,6 +53,7 @@ func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) ( SyncResponse: syncResponse, LogPath: s.logFile, CPUProfile: cpuProfileData, + RefreshStatus: refreshStatus, }, debug.BundleConfig{ Anonymize: req.GetAnonymize(), diff --git a/client/server/server.go b/client/server/server.go index e3c95077a..b291d7f71 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -1327,6 +1327,10 @@ func (s *Server) runProbes(waitForProbeResult bool) { if engine.RunHealthProbes(waitForProbeResult) { s.lastProbe = time.Now() } + } else { + if err := s.statusRecorder.RefreshWireGuardStats(); err != nil { + log.Debugf("failed to refresh WireGuard stats: %v", err) + } } } From 737d6061bffe8a748c2317c1f8b9469783c130b1 Mon Sep 17 00:00:00 2001 From: Vlad <4941176+crn4@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:05:22 +0100 Subject: [PATCH 2/4] [management] ephemeral peers track on login (#5165) --- .../network_map/controller/controller.go | 4 ++++ .../controllers/network_map/interface.go | 2 ++ .../controllers/network_map/interface_mock.go | 16 ++++++++++++++-- management/server/peer.go | 5 +++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/management/internals/controllers/network_map/controller/controller.go b/management/internals/controllers/network_map/controller/controller.go index d46737c26..5ae64e9f1 100644 --- a/management/internals/controllers/network_map/controller/controller.go +++ b/management/internals/controllers/network_map/controller/controller.go @@ -856,3 +856,7 @@ func (c *Controller) GetNetworkMap(ctx context.Context, peerID string) (*types.N func (c *Controller) DisconnectPeers(ctx context.Context, accountId string, peerIDs []string) { c.peersUpdateManager.CloseChannels(ctx, peerIDs) } + +func (c *Controller) TrackEphemeralPeer(ctx context.Context, peer *nbpeer.Peer) { + c.EphemeralPeersManager.OnPeerDisconnected(ctx, peer) +} diff --git a/management/internals/controllers/network_map/interface.go b/management/internals/controllers/network_map/interface.go index b1de7d017..64caac861 100644 --- a/management/internals/controllers/network_map/interface.go +++ b/management/internals/controllers/network_map/interface.go @@ -36,4 +36,6 @@ type Controller interface { DisconnectPeers(ctx context.Context, accountId string, peerIDs []string) OnPeerConnected(ctx context.Context, accountID string, peerID string) (chan *UpdateMessage, error) OnPeerDisconnected(ctx context.Context, accountID string, peerID string) + + TrackEphemeralPeer(ctx context.Context, peer *nbpeer.Peer) } diff --git a/management/internals/controllers/network_map/interface_mock.go b/management/internals/controllers/network_map/interface_mock.go index 5a98eefa8..4e86d2973 100644 --- a/management/internals/controllers/network_map/interface_mock.go +++ b/management/internals/controllers/network_map/interface_mock.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: ./interface.go +// Source: management/internals/controllers/network_map/interface.go // // Generated by this command: // -// mockgen -package network_map -destination=interface_mock.go -source=./interface.go -build_flags=-mod=mod +// mockgen -package network_map -destination=management/internals/controllers/network_map/interface_mock.go -source=management/internals/controllers/network_map/interface.go -build_flags=-mod=mod // // Package network_map is a generated GoMock package. @@ -211,6 +211,18 @@ func (mr *MockControllerMockRecorder) StartWarmup(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartWarmup", reflect.TypeOf((*MockController)(nil).StartWarmup), arg0) } +// TrackEphemeralPeer mocks base method. +func (m *MockController) TrackEphemeralPeer(ctx context.Context, arg1 *peer.Peer) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "TrackEphemeralPeer", ctx, arg1) +} + +// TrackEphemeralPeer indicates an expected call of TrackEphemeralPeer. +func (mr *MockControllerMockRecorder) TrackEphemeralPeer(ctx, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TrackEphemeralPeer", reflect.TypeOf((*MockController)(nil).TrackEphemeralPeer), ctx, arg1) +} + // UpdateAccountPeer mocks base method. func (m *MockController) UpdateAccountPeer(ctx context.Context, accountId, peerId string) error { m.ctrl.T.Helper() diff --git a/management/server/peer.go b/management/server/peer.go index d6eb2aecd..80c74e209 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -728,6 +728,11 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe return fmt.Errorf("failed adding peer to All group: %w", err) } + if temporary { + // we should track ephemeral peers to be able to clean them if the peer don't sync and be marked as connected + am.networkMapController.TrackEphemeralPeer(ctx, newPeer) + } + if addedByUser { err := transaction.SaveUserLastLogin(ctx, accountID, userID, newPeer.GetLastLogin()) if err != nil { From c61568ceb4ad2ae9c3dd31ae6b689fff81e0e336 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 23 Jan 2026 18:06:54 +0100 Subject: [PATCH 3/4] [client] Change default rosenpass log level (#5137) * Change default rosenpass log level - Add support to environment configuration - Change default log level to info * use .String() for print log level --- client/internal/rosenpass/manager.go | 29 ++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/client/internal/rosenpass/manager.go b/client/internal/rosenpass/manager.go index 26a1eef58..1faa22dc5 100644 --- a/client/internal/rosenpass/manager.go +++ b/client/internal/rosenpass/manager.go @@ -17,6 +17,11 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) +const ( + defaultLog = slog.LevelInfo + defaultLogLevelVar = "NB_ROSENPASS_LOG_LEVEL" +) + func hashRosenpassKey(key []byte) string { hasher := sha256.New() hasher.Write(key) @@ -45,7 +50,7 @@ func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string) (*Manager, error) } rpKeyHash := hashRosenpassKey(public) - log.Debugf("generated new rosenpass key pair with public key %s", rpKeyHash) + log.Tracef("generated new rosenpass key pair with public key %s", rpKeyHash) return &Manager{ifaceName: wgIfaceName, rpKeyHash: rpKeyHash, spk: public, ssk: secret, preSharedKey: (*[32]byte)(preSharedKey), rpPeerIDs: make(map[string]*rp.PeerID), lock: sync.Mutex{}}, nil } @@ -101,7 +106,7 @@ func (m *Manager) removePeer(wireGuardPubKey string) error { func (m *Manager) generateConfig() (rp.Config, error) { opts := &slog.HandlerOptions{ - Level: slog.LevelDebug, + Level: getLogLevel(), } logger := slog.New(slog.NewTextHandler(os.Stdout, opts)) cfg := rp.Config{Logger: logger} @@ -133,6 +138,26 @@ func (m *Manager) generateConfig() (rp.Config, error) { return cfg, nil } +func getLogLevel() slog.Level { + level, ok := os.LookupEnv(defaultLogLevelVar) + if !ok { + return defaultLog + } + switch strings.ToLower(level) { + case "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + log.Warnf("unknown log level: %s. Using default %s", level, defaultLog.String()) + return defaultLog + } +} + func (m *Manager) OnDisconnected(peerKey string) { m.lock.Lock() defer m.lock.Unlock() From 67211010f7240d53734abd922777c32fccb02754 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 23 Jan 2026 18:39:45 +0100 Subject: [PATCH 4/4] [client, gui] fix exit nodes menu on reconnect, remove tooltips (#5167) * [client, gui] fix exit nodes menu on reconnect clean s.exitNodeStates when disconnecting * disable tooltip for exit nodes and settings --- client/ui/client_ui.go | 5 ++--- client/ui/const.go | 4 +--- client/ui/event_handler.go | 2 ++ client/ui/network.go | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 5d955ed25..0290e17d5 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -1033,7 +1033,7 @@ func (s *serviceClient) onTrayReady() { s.mDown.Disable() systray.AddSeparator() - s.mSettings = systray.AddMenuItem("Settings", settingsMenuDescr) + s.mSettings = systray.AddMenuItem("Settings", disabledMenuDescr) s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", allowSSHMenuDescr, false) s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", autoConnectMenuDescr, false) s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", quantumResistanceMenuDescr, false) @@ -1060,7 +1060,7 @@ func (s *serviceClient) onTrayReady() { } s.exitNodeMu.Lock() - s.mExitNode = systray.AddMenuItem("Exit Node", exitNodeMenuDescr) + s.mExitNode = systray.AddMenuItem("Exit Node", disabledMenuDescr) s.mExitNode.Disable() s.exitNodeMu.Unlock() @@ -1261,7 +1261,6 @@ func (s *serviceClient) setSettingsEnabled(enabled bool) { if s.mSettings != nil { if enabled { s.mSettings.Enable() - s.mSettings.SetTooltip(settingsMenuDescr) } else { s.mSettings.Hide() s.mSettings.SetTooltip("Settings are disabled by daemon") diff --git a/client/ui/const.go b/client/ui/const.go index 332282c17..48619be75 100644 --- a/client/ui/const.go +++ b/client/ui/const.go @@ -1,8 +1,6 @@ package main const ( - settingsMenuDescr = "Settings of the application" - profilesMenuDescr = "Manage your profiles" allowSSHMenuDescr = "Allow SSH connections" autoConnectMenuDescr = "Connect automatically when the service starts" quantumResistanceMenuDescr = "Enable post-quantum security via Rosenpass" @@ -11,7 +9,7 @@ const ( notificationsMenuDescr = "Enable notifications" advancedSettingsMenuDescr = "Advanced settings of the application" debugBundleMenuDescr = "Create and open debug information bundle" - exitNodeMenuDescr = "Select exit node for routing traffic" + disabledMenuDescr = "" networksMenuDescr = "Open the networks management window" latestVersionMenuDescr = "Download latest version" quitMenuDescr = "Quit the client app" diff --git a/client/ui/event_handler.go b/client/ui/event_handler.go index 9ffacd926..cc55c31dd 100644 --- a/client/ui/event_handler.go +++ b/client/ui/event_handler.go @@ -99,6 +99,8 @@ func (h *eventHandler) handleConnectClick() { func (h *eventHandler) handleDisconnectClick() { h.client.mDown.Disable() + h.client.exitNodeStates = []exitNodeState{} + if h.client.connectCancel != nil { log.Debugf("cancelling ongoing connect operation") h.client.connectCancel() diff --git a/client/ui/network.go b/client/ui/network.go index fb73efd7b..371eb975b 100644 --- a/client/ui/network.go +++ b/client/ui/network.go @@ -390,7 +390,7 @@ func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) { if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { s.mExitNode.Remove() - s.mExitNode = systray.AddMenuItem("Exit Node", exitNodeMenuDescr) + s.mExitNode = systray.AddMenuItem("Exit Node", disabledMenuDescr) } var showDeselectAll bool