Merge branch 'main' into log/conn-disconn

This commit is contained in:
crn4
2026-01-23 19:59:25 +01:00
14 changed files with 120 additions and 29 deletions
+7
View File
@@ -228,6 +228,7 @@ type BundleGenerator struct {
syncResponse *mgmProto.SyncResponse syncResponse *mgmProto.SyncResponse
logPath string logPath string
cpuProfile []byte cpuProfile []byte
refreshStatus func() // Optional callback to refresh status before bundle generation
anonymize bool anonymize bool
includeSystemInfo bool includeSystemInfo bool
@@ -248,6 +249,7 @@ type GeneratorDependencies struct {
SyncResponse *mgmProto.SyncResponse SyncResponse *mgmProto.SyncResponse
LogPath string LogPath string
CPUProfile []byte CPUProfile []byte
RefreshStatus func() // Optional callback to refresh status before bundle generation
} }
func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGenerator { func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGenerator {
@@ -265,6 +267,7 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen
syncResponse: deps.SyncResponse, syncResponse: deps.SyncResponse,
logPath: deps.LogPath, logPath: deps.LogPath,
cpuProfile: deps.CPUProfile, cpuProfile: deps.CPUProfile,
refreshStatus: deps.RefreshStatus,
anonymize: cfg.Anonymize, anonymize: cfg.Anonymize,
includeSystemInfo: cfg.IncludeSystemInfo, includeSystemInfo: cfg.IncludeSystemInfo,
@@ -408,6 +411,10 @@ func (g *BundleGenerator) addStatus() error {
profName = activeProf.Name profName = activeProf.Name
} }
if g.refreshStatus != nil {
g.refreshStatus()
}
fullStatus := g.statusRecorder.GetFullStatus() fullStatus := g.statusRecorder.GetFullStatus()
protoFullStatus := nbstatus.ToProtoFullStatus(fullStatus) protoFullStatus := nbstatus.ToProtoFullStatus(fullStatus)
protoFullStatus.Events = g.statusRecorder.GetEventHistory() protoFullStatus.Events = g.statusRecorder.GetEventHistory()
+6 -18
View File
@@ -1050,6 +1050,9 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR
StatusRecorder: e.statusRecorder, StatusRecorder: e.statusRecorder,
SyncResponse: syncResponse, SyncResponse: syncResponse,
LogPath: e.config.LogPath, LogPath: e.config.LogPath,
RefreshStatus: func() {
e.RunHealthProbes(true)
},
} }
bundleJobParams := debug.BundleConfig{ bundleJobParams := debug.BundleConfig{
@@ -1827,7 +1830,7 @@ func (e *Engine) getRosenpassAddr() string {
return "" 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. // and updates the status recorder with the latest states.
func (e *Engine) RunHealthProbes(waitForResult bool) bool { func (e *Engine) RunHealthProbes(waitForResult bool) bool {
e.syncMsgMux.Lock() e.syncMsgMux.Lock()
@@ -1841,23 +1844,8 @@ func (e *Engine) RunHealthProbes(waitForResult bool) bool {
stuns := slices.Clone(e.STUNs) stuns := slices.Clone(e.STUNs)
turns := slices.Clone(e.TURNs) turns := slices.Clone(e.TURNs)
if e.wgInterface != nil { if err := e.statusRecorder.RefreshWireGuardStats(); err != nil {
stats, err := e.wgInterface.GetStats() log.Debugf("failed to refresh WireGuard stats: %v", err)
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)
}
}
} }
e.syncMsgMux.Unlock() e.syncMsgMux.Unlock()
+32
View File
@@ -1145,6 +1145,38 @@ func (d *Status) PeersStatus() (*configurer.Stats, error) {
return d.wgIface.FullStats() 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 { type EventQueue struct {
maxSize int maxSize int
events []*proto.SystemEvent events []*proto.SystemEvent
+27 -2
View File
@@ -17,6 +17,11 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )
const (
defaultLog = slog.LevelInfo
defaultLogLevelVar = "NB_ROSENPASS_LOG_LEVEL"
)
func hashRosenpassKey(key []byte) string { func hashRosenpassKey(key []byte) string {
hasher := sha256.New() hasher := sha256.New()
hasher.Write(key) hasher.Write(key)
@@ -45,7 +50,7 @@ func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string) (*Manager, error)
} }
rpKeyHash := hashRosenpassKey(public) 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 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) { func (m *Manager) generateConfig() (rp.Config, error) {
opts := &slog.HandlerOptions{ opts := &slog.HandlerOptions{
Level: slog.LevelDebug, Level: getLogLevel(),
} }
logger := slog.New(slog.NewTextHandler(os.Stdout, opts)) logger := slog.New(slog.NewTextHandler(os.Stdout, opts))
cfg := rp.Config{Logger: logger} cfg := rp.Config{Logger: logger}
@@ -133,6 +138,26 @@ func (m *Manager) generateConfig() (rp.Config, error) {
return cfg, nil 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) { func (m *Manager) OnDisconnected(peerKey string) {
m.lock.Lock() m.lock.Lock()
defer m.lock.Unlock() defer m.lock.Unlock()
+13
View File
@@ -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( bundleGenerator := debug.NewBundleGenerator(
debug.GeneratorDependencies{ debug.GeneratorDependencies{
InternalConfig: s.config, InternalConfig: s.config,
@@ -41,6 +53,7 @@ func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) (
SyncResponse: syncResponse, SyncResponse: syncResponse,
LogPath: s.logFile, LogPath: s.logFile,
CPUProfile: cpuProfileData, CPUProfile: cpuProfileData,
RefreshStatus: refreshStatus,
}, },
debug.BundleConfig{ debug.BundleConfig{
Anonymize: req.GetAnonymize(), Anonymize: req.GetAnonymize(),
+4
View File
@@ -1327,6 +1327,10 @@ func (s *Server) runProbes(waitForProbeResult bool) {
if engine.RunHealthProbes(waitForProbeResult) { if engine.RunHealthProbes(waitForProbeResult) {
s.lastProbe = time.Now() s.lastProbe = time.Now()
} }
} else {
if err := s.statusRecorder.RefreshWireGuardStats(); err != nil {
log.Debugf("failed to refresh WireGuard stats: %v", err)
}
} }
} }
+2 -3
View File
@@ -1033,7 +1033,7 @@ func (s *serviceClient) onTrayReady() {
s.mDown.Disable() s.mDown.Disable()
systray.AddSeparator() systray.AddSeparator()
s.mSettings = systray.AddMenuItem("Settings", settingsMenuDescr) s.mSettings = systray.AddMenuItem("Settings", disabledMenuDescr)
s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", allowSSHMenuDescr, false) s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", allowSSHMenuDescr, false)
s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", autoConnectMenuDescr, false) s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", autoConnectMenuDescr, false)
s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", quantumResistanceMenuDescr, false) s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", quantumResistanceMenuDescr, false)
@@ -1060,7 +1060,7 @@ func (s *serviceClient) onTrayReady() {
} }
s.exitNodeMu.Lock() s.exitNodeMu.Lock()
s.mExitNode = systray.AddMenuItem("Exit Node", exitNodeMenuDescr) s.mExitNode = systray.AddMenuItem("Exit Node", disabledMenuDescr)
s.mExitNode.Disable() s.mExitNode.Disable()
s.exitNodeMu.Unlock() s.exitNodeMu.Unlock()
@@ -1261,7 +1261,6 @@ func (s *serviceClient) setSettingsEnabled(enabled bool) {
if s.mSettings != nil { if s.mSettings != nil {
if enabled { if enabled {
s.mSettings.Enable() s.mSettings.Enable()
s.mSettings.SetTooltip(settingsMenuDescr)
} else { } else {
s.mSettings.Hide() s.mSettings.Hide()
s.mSettings.SetTooltip("Settings are disabled by daemon") s.mSettings.SetTooltip("Settings are disabled by daemon")
+1 -3
View File
@@ -1,8 +1,6 @@
package main package main
const ( const (
settingsMenuDescr = "Settings of the application"
profilesMenuDescr = "Manage your profiles"
allowSSHMenuDescr = "Allow SSH connections" allowSSHMenuDescr = "Allow SSH connections"
autoConnectMenuDescr = "Connect automatically when the service starts" autoConnectMenuDescr = "Connect automatically when the service starts"
quantumResistanceMenuDescr = "Enable post-quantum security via Rosenpass" quantumResistanceMenuDescr = "Enable post-quantum security via Rosenpass"
@@ -11,7 +9,7 @@ const (
notificationsMenuDescr = "Enable notifications" notificationsMenuDescr = "Enable notifications"
advancedSettingsMenuDescr = "Advanced settings of the application" advancedSettingsMenuDescr = "Advanced settings of the application"
debugBundleMenuDescr = "Create and open debug information bundle" debugBundleMenuDescr = "Create and open debug information bundle"
exitNodeMenuDescr = "Select exit node for routing traffic" disabledMenuDescr = ""
networksMenuDescr = "Open the networks management window" networksMenuDescr = "Open the networks management window"
latestVersionMenuDescr = "Download latest version" latestVersionMenuDescr = "Download latest version"
quitMenuDescr = "Quit the client app" quitMenuDescr = "Quit the client app"
+2
View File
@@ -99,6 +99,8 @@ func (h *eventHandler) handleConnectClick() {
func (h *eventHandler) handleDisconnectClick() { func (h *eventHandler) handleDisconnectClick() {
h.client.mDown.Disable() h.client.mDown.Disable()
h.client.exitNodeStates = []exitNodeState{}
if h.client.connectCancel != nil { if h.client.connectCancel != nil {
log.Debugf("cancelling ongoing connect operation") log.Debugf("cancelling ongoing connect operation")
h.client.connectCancel() h.client.connectCancel()
+1 -1
View File
@@ -390,7 +390,7 @@ func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) {
if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" {
s.mExitNode.Remove() s.mExitNode.Remove()
s.mExitNode = systray.AddMenuItem("Exit Node", exitNodeMenuDescr) s.mExitNode = systray.AddMenuItem("Exit Node", disabledMenuDescr)
} }
var showDeselectAll bool var showDeselectAll bool
@@ -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) { func (c *Controller) DisconnectPeers(ctx context.Context, accountId string, peerIDs []string) {
c.peersUpdateManager.CloseChannels(ctx, peerIDs) c.peersUpdateManager.CloseChannels(ctx, peerIDs)
} }
func (c *Controller) TrackEphemeralPeer(ctx context.Context, peer *nbpeer.Peer) {
c.EphemeralPeersManager.OnPeerDisconnected(ctx, peer)
}
@@ -36,4 +36,6 @@ type Controller interface {
DisconnectPeers(ctx context.Context, accountId string, peerIDs []string) DisconnectPeers(ctx context.Context, accountId string, peerIDs []string)
OnPeerConnected(ctx context.Context, accountID string, peerID string) (chan *UpdateMessage, error) OnPeerConnected(ctx context.Context, accountID string, peerID string) (chan *UpdateMessage, error)
OnPeerDisconnected(ctx context.Context, accountID string, peerID string) OnPeerDisconnected(ctx context.Context, accountID string, peerID string)
TrackEphemeralPeer(ctx context.Context, peer *nbpeer.Peer)
} }
@@ -1,9 +1,9 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: ./interface.go // Source: management/internals/controllers/network_map/interface.go
// //
// Generated by this command: // 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. // 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) 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. // UpdateAccountPeer mocks base method.
func (m *MockController) UpdateAccountPeer(ctx context.Context, accountId, peerId string) error { func (m *MockController) UpdateAccountPeer(ctx context.Context, accountId, peerId string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
+5
View File
@@ -746,6 +746,11 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
return fmt.Errorf("failed adding peer to All group: %w", err) 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 { if addedByUser {
err := transaction.SaveUserLastLogin(ctx, accountID, userID, newPeer.GetLastLogin()) err := transaction.SaveUserLastLogin(ctx, accountID, userID, newPeer.GetLastLogin())
if err != nil { if err != nil {