Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions client/android/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import (
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/formatter"
"github.com/netbirdio/netbird/client/net"
)

// ConnectionListener export internal Listener for mobile
Expand Down Expand Up @@ -114,7 +114,7 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead

// todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx)
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder, false)
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener)
}

Expand All @@ -141,7 +141,7 @@ func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener

// todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx)
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder, false)
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener)
}

Expand Down
2 changes: 1 addition & 1 deletion client/cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *pr
r := peer.NewRecorder(config.ManagementURL.String())
r.GetFullStatus()

connectClient := internal.NewConnectClient(ctx, config, r)
connectClient := internal.NewConnectClient(ctx, config, r, false)
SetupDebugHandler(ctx, config, r, connectClient, "")

return connectClient.Run(nil)
Expand Down
2 changes: 1 addition & 1 deletion client/embed/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func (c *Client) Start(startCtx context.Context) error {
}

recorder := peer.NewRecorder(c.config.ManagementURL.String())
client := internal.NewConnectClient(ctx, c.config, recorder)
client := internal.NewConnectClient(ctx, c.config, recorder, false)

// either startup error (permanent backoff err) or nil err (successful engine up)
// TODO: make after-startup backoff err available
Expand Down
38 changes: 28 additions & 10 deletions client/internal/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/internal/updatemanager/installer"
nbnet "github.com/netbirdio/netbird/client/net"
cProto "github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/ssh"
Expand All @@ -39,11 +40,13 @@ import (
)

type ConnectClient struct {
ctx context.Context
config *profilemanager.Config
statusRecorder *peer.Status
engine *Engine
engineMutex sync.Mutex
ctx context.Context
config *profilemanager.Config
statusRecorder *peer.Status
doInitialAutoUpdate bool

engine *Engine
engineMutex sync.Mutex

persistSyncResponse bool
}
Expand All @@ -52,13 +55,15 @@ func NewConnectClient(
ctx context.Context,
config *profilemanager.Config,
statusRecorder *peer.Status,
doInitalAutoUpdate bool,

) *ConnectClient {
return &ConnectClient{
ctx: ctx,
config: config,
statusRecorder: statusRecorder,
engineMutex: sync.Mutex{},
ctx: ctx,
config: config,
statusRecorder: statusRecorder,
doInitialAutoUpdate: doInitalAutoUpdate,
engineMutex: sync.Mutex{},
}
}

Expand Down Expand Up @@ -275,13 +280,26 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
c.engine.SetSyncResponsePersistence(c.persistSyncResponse)
c.engineMutex.Unlock()

inst, err := installer.New()
if err == nil {
// todo consider to keep result file but somehow prevent ui to show error again
if err := inst.CleanUpInstallerFiles(); err != nil {
log.Errorf("failed to clean up temporary installer file: %v", err)
}
}

if err := c.engine.Start(loginResp.GetNetbirdConfig(), c.config.ManagementURL); err != nil {
log.Errorf("error while starting Netbird Connection Engine: %s", err)
return wrapErr(err)
}

if loginResp.PeerConfig != nil && loginResp.PeerConfig.AutoUpdate != nil {
c.engine.InitialUpdateHandling(loginResp.PeerConfig.AutoUpdate)
// AutoUpdate will be true when the user click on "Connect" menu on the UI
if c.doInitialAutoUpdate {
log.Infof("start engine by ui, run auto-update check")
c.engine.InitialUpdateHandling(loginResp.PeerConfig.AutoUpdate)
c.doInitialAutoUpdate = false
}
}

log.Infof("Netbird engine started, the IP is: %s", peerConfig.GetAddress())
Expand Down
30 changes: 30 additions & 0 deletions client/internal/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/netbirdio/netbird/client/anonymize"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/updatemanager/installer"
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/util"
)
Expand Down Expand Up @@ -338,6 +339,10 @@ func (g *BundleGenerator) createArchive() error {
log.Errorf("failed to add systemd logs: %v", err)
}

if err := g.addUpdateLogs(); err != nil {
log.Errorf("failed to add updater logs: %v", err)
}

return nil
}

Expand Down Expand Up @@ -598,6 +603,31 @@ func (g *BundleGenerator) addStateFile() error {
return nil
}

func (g *BundleGenerator) addUpdateLogs() error {
inst, err := installer.New()
if err != nil {
// unsupported platform
// nolint
return nil
}
log.Infof("adding updater logs")
logFiles := inst.LogFiles()

for _, logFile := range logFiles {
data, err := os.ReadFile(logFile)
if err != nil {
log.Warnf("failed to read update log file %s: %v", logFile, err)
continue
}

baseName := filepath.Base(logFile)
if err := g.addFileToZip(bytes.NewReader(data), filepath.Join("update-logs", baseName)); err != nil {
return fmt.Errorf("add update log file %s to zip: %w", baseName, err)
}
}
return nil
}

func (g *BundleGenerator) addCorruptedStateFiles() error {
sm := profilemanager.NewServiceManager("")
pattern := sm.GetStatePath()
Expand Down
6 changes: 3 additions & 3 deletions client/internal/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ type Engine struct {
flowManager nftypes.FlowManager

// auto-update
updateManager *updatemanager.UpdateManager
updateManager *updatemanager.Manager

// WireGuard interface monitor
wgIfaceMonitor *WGIfaceMonitor
Expand Down Expand Up @@ -514,7 +514,7 @@ func (e *Engine) InitialUpdateHandling(autoUpdateSettings *mgmProto.AutoUpdateSe
defer e.syncMsgMux.Unlock()

if e.updateManager == nil {
e.updateManager = updatemanager.NewUpdateManager(e.statusRecorder, e.stateManager)
e.updateManager = updatemanager.NewManager(e.statusRecorder, e.stateManager)
}

e.updateManager.CheckUpdateSuccess(e.ctx)
Expand Down Expand Up @@ -758,7 +758,7 @@ func (e *Engine) handleAutoUpdateVersion(autoUpdateSettings *mgmProto.AutoUpdate
// Start manager if needed
if e.updateManager == nil {
log.Infof("starting auto-update manager")
e.updateManager = updatemanager.NewUpdateManager(e.statusRecorder, e.stateManager)
e.updateManager = updatemanager.NewManager(e.statusRecorder, e.stateManager)
}
e.updateManager.Start(e.ctx)
log.Infof("handling auto-update version: %s", autoUpdateSettings.Version)
Expand Down
35 changes: 35 additions & 0 deletions client/internal/updatemanager/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Package updatemanager provides automatic update management for the NetBird client.
// It monitors for new versions, handles update triggers from management server directives,
// and orchestrates the download and installation of client updates.
//
// # Overview
//
// The update manager operates as a background service that continuously monitors for
// available updates and automatically initiates the update process when conditions are met.
// It integrates with the installer package to perform the actual installation.
//
// # Update Flow
//
// The complete update process follows these steps:
//
// 1. Manager receives update directive via SetVersion() or detects new version
// 2. Manager validates update should proceed (version comparison, rate limiting)
// 3. Manager publishes "updating" event to status recorder
// 4. Manager persists UpdateState to track update attempt
// 5. Manager downloads installer file (.msi or .exe) to temporary directory
// 6. Manager triggers installation via installer.RunInstallation()
// 7. Installer package handles the actual installation process
// 8. On next startup, CheckUpdateSuccess() verifies update completion
// 9. Manager publishes success/failure event to status recorder
// 10. Manager cleans up UpdateState
//
// # State Management
//
// Update state is persisted across restarts to track update attempts:
//
// - PreUpdateVersion: Version before update attempt
// - TargetVersion: Version attempting to update to
//
// This enables verification of successful updates and appropriate user notification
// after the client restarts with the new version.
package updatemanager
7 changes: 7 additions & 0 deletions client/internal/updatemanager/installer/binary_nowindows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build !windows

package installer

func UpdaterBinaryNameWithoutExtension() string {
return updaterBinary
}
11 changes: 11 additions & 0 deletions client/internal/updatemanager/installer/binary_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package installer

import (
"path/filepath"
"strings"
)

func UpdaterBinaryNameWithoutExtension() string {
ext := filepath.Ext(updaterBinary)
return strings.TrimSuffix(updaterBinary, ext)
}
46 changes: 46 additions & 0 deletions client/internal/updatemanager/installer/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Package installer provides functionality for managing application updates and installations .
// It handles the complete update lifecycle including installation execution,
// process management, and result reporting.
//
// # Architecture
//
// The installer package uses a two-process architecture:
//
// 1. Service Process: The main application process that initiates updates
// 2. Updater Process: A detached process that performs the actual installation
//
// This separation ensures that the application can be updated while running, and allows
// the updater to restart the application after installation completes.
//
// # Update Flow
//
// The typical update flow follows these steps:
//
// 1. Service calls RunInstallation() with an installer file (.exe or .msi)
// 2. Service copies itself to tempDir as "updater.exe"
// 3. Service launches updater.exe as a detached process with installation parameters
// 4. Updater executes the installer (silently for both .exe and .msi)
// 5. Installer will kill the daemon and UI and it will try to restart it
// 6. Updater restarts the daemon and UI after installation
// 7. Updater writes result.json with success/failure status
// 8. Service watches for result.json using Watch() to get installation outcome
//
// # File Locations
//
// Default temporary directory (Windows):
// - %ProgramData%\Netbird\tmp-install
//
// Files created during installation:
// - updater.exe: Copy of the service binary used to run installation
// - result.json: Installation result with success status and error messages
// - msi.log: Verbose MSI installer log (MSI installations only)
// - installer.log: General installer operation log
//
// # Cleanup
//
// The CleanUpInstallerFiles() function removes temporary files:
//
// - Installer binaries (.exe, .msi)
// - Updater binary copy
// - Result files
package installer
46 changes: 46 additions & 0 deletions client/internal/updatemanager/installer/installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//go:build !windows && !darwin

package installer

import (
"context"
"fmt"
)

const (
updaterBinary = "updater"
)

type Installer struct {
tempDir string
}

// New used by the service
func New() (*Installer, error) {
return nil, fmt.Errorf("unsupported platform")
}

// NewWithDir used by the updater process, get the tempDir from the service via cmd line
func NewWithDir(tempDir string) *Installer {
return &Installer{
tempDir: tempDir,
}
}

func (c *Installer) LogFiles() []string {
return []string{}
}

func (u *Installer) CleanUpInstallerFiles() error {
return nil
}

func (u *Installer) RunInstallation(targetVersion string) error {
return fmt.Errorf("unsupported platform")
}

// Setup runs the installer with appropriate arguments and manages the daemon/UI state
// This will be run by the updater process
func (u *Installer) Setup(ctx context.Context, dryRun bool, targetVersion string, daemonFolder string) (resultErr error) {
return fmt.Errorf("not supported")
}
Loading
Loading