[client] refactor auto update workflow (#5448)

Auto-update logic moved out of the UI into a dedicated updatemanager.Manager service that runs in the connection layer. The
UI no longer polls or checks for updates independently.
The update manager supports three modes driven by the management server's auto-update policy:
No policy set by mgm: checks GitHub for the latest version and notifies the user (previous behavior, now centralized)
mgm enforces update: the "About" menu triggers installation directly instead of just downloading the file — user still initiates the action
mgm forces update: installation proceeds automatically without user interaction
updateManager lifecycle is now owned by daemon, giving the daemon server direct control via a new TriggerUpdate RPC
Introduces EngineServices struct to group external service dependencies passed to NewEngine, reducing its argument count from 11 to 4
This commit is contained in:
Zoltan Papp
2026-03-13 17:01:28 +01:00
committed by GitHub
parent 2e1aa497d2
commit fe9b844511
84 changed files with 1210 additions and 626 deletions

View File

@@ -124,7 +124,7 @@ func (c *Client) Run(platformFiles PlatformFiles, urlOpener URLOpener, isAndroid
// todo do not throw error in case of cancelled context // todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx) ctx = internal.CtxInitState(ctx)
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder, false) c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile) return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile)
} }
@@ -157,7 +157,7 @@ func (c *Client) RunWithoutLogin(platformFiles PlatformFiles, dns *DNSList, dnsR
// todo do not throw error in case of cancelled context // todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx) ctx = internal.CtxInitState(ctx)
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder, false) c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile) return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile)
} }

View File

@@ -7,7 +7,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal/updatemanager/reposign" "github.com/netbirdio/netbird/client/internal/updater/reposign"
) )
var ( var (

View File

@@ -6,7 +6,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal/updatemanager/reposign" "github.com/netbirdio/netbird/client/internal/updater/reposign"
) )
const ( const (

View File

@@ -7,7 +7,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal/updatemanager/reposign" "github.com/netbirdio/netbird/client/internal/updater/reposign"
) )
const ( const (

View File

@@ -7,7 +7,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal/updatemanager/reposign" "github.com/netbirdio/netbird/client/internal/updater/reposign"
) )
var ( var (

View File

@@ -197,7 +197,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *pr
r := peer.NewRecorder(config.ManagementURL.String()) r := peer.NewRecorder(config.ManagementURL.String())
r.GetFullStatus() r.GetFullStatus()
connectClient := internal.NewConnectClient(ctx, config, r, false) connectClient := internal.NewConnectClient(ctx, config, r)
SetupDebugHandler(ctx, config, r, connectClient, "") SetupDebugHandler(ctx, config, r, connectClient, "")
return connectClient.Run(nil, util.FindFirstLogPath(logFiles)) return connectClient.Run(nil, util.FindFirstLogPath(logFiles))

View File

@@ -11,7 +11,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal/updatemanager/installer" "github.com/netbirdio/netbird/client/internal/updater/installer"
"github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/util"
) )

View File

@@ -202,7 +202,7 @@ func (c *Client) Start(startCtx context.Context) error {
if err, _ := authClient.Login(ctx, c.setupKey, c.jwtToken); err != nil { if err, _ := authClient.Login(ctx, c.setupKey, c.jwtToken); err != nil {
return fmt.Errorf("login: %w", err) return fmt.Errorf("login: %w", err)
} }
client := internal.NewConnectClient(ctx, c.config, c.recorder, false) client := internal.NewConnectClient(ctx, c.config, c.recorder)
client.SetSyncResponsePersistence(true) client.SetSyncResponsePersistence(true)
// either startup error (permanent backoff err) or nil err (successful engine up) // either startup error (permanent backoff err) or nil err (successful engine up)

View File

@@ -27,8 +27,8 @@ import (
"github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/client/internal/statemanager"
"github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/internal/updatemanager" "github.com/netbirdio/netbird/client/internal/updater"
"github.com/netbirdio/netbird/client/internal/updatemanager/installer" "github.com/netbirdio/netbird/client/internal/updater/installer"
nbnet "github.com/netbirdio/netbird/client/net" nbnet "github.com/netbirdio/netbird/client/net"
cProto "github.com/netbirdio/netbird/client/proto" cProto "github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/ssh"
@@ -47,10 +47,10 @@ type ConnectClient struct {
ctx context.Context ctx context.Context
config *profilemanager.Config config *profilemanager.Config
statusRecorder *peer.Status statusRecorder *peer.Status
doInitialAutoUpdate bool
engine *Engine engine *Engine
engineMutex sync.Mutex engineMutex sync.Mutex
updateManager *updater.Manager
persistSyncResponse bool persistSyncResponse bool
} }
@@ -59,17 +59,19 @@ func NewConnectClient(
ctx context.Context, ctx context.Context,
config *profilemanager.Config, config *profilemanager.Config,
statusRecorder *peer.Status, statusRecorder *peer.Status,
doInitalAutoUpdate bool,
) *ConnectClient { ) *ConnectClient {
return &ConnectClient{ return &ConnectClient{
ctx: ctx, ctx: ctx,
config: config, config: config,
statusRecorder: statusRecorder, statusRecorder: statusRecorder,
doInitialAutoUpdate: doInitalAutoUpdate,
engineMutex: sync.Mutex{}, engineMutex: sync.Mutex{},
} }
} }
func (c *ConnectClient) SetUpdateManager(um *updater.Manager) {
c.updateManager = um
}
// Run with main logic. // Run with main logic.
func (c *ConnectClient) Run(runningChan chan struct{}, logPath string) error { func (c *ConnectClient) Run(runningChan chan struct{}, logPath string) error {
return c.run(MobileDependency{}, runningChan, logPath) return c.run(MobileDependency{}, runningChan, logPath)
@@ -187,15 +189,14 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
stateManager := statemanager.New(path) stateManager := statemanager.New(path)
stateManager.RegisterState(&sshconfig.ShutdownState{}) stateManager.RegisterState(&sshconfig.ShutdownState{})
updateManager, err := updatemanager.NewManager(c.statusRecorder, stateManager) if c.updateManager != nil {
if err == nil { c.updateManager.CheckUpdateSuccess(c.ctx)
updateManager.CheckUpdateSuccess(c.ctx) }
inst := installer.New() inst := installer.New()
if err := inst.CleanUpInstallerFiles(); err != nil { if err := inst.CleanUpInstallerFiles(); err != nil {
log.Errorf("failed to clean up temporary installer file: %v", err) log.Errorf("failed to clean up temporary installer file: %v", err)
} }
}
defer c.statusRecorder.ClientStop() defer c.statusRecorder.ClientStop()
operation := func() error { operation := func() error {
@@ -308,7 +309,15 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
checks := loginResp.GetChecks() checks := loginResp.GetChecks()
c.engineMutex.Lock() c.engineMutex.Lock()
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, checks, stateManager) engine := NewEngine(engineCtx, cancel, engineConfig, EngineServices{
SignalClient: signalClient,
MgmClient: mgmClient,
RelayManager: relayManager,
StatusRecorder: c.statusRecorder,
Checks: checks,
StateManager: stateManager,
UpdateManager: c.updateManager,
}, mobileDependency)
engine.SetSyncResponsePersistence(c.persistSyncResponse) engine.SetSyncResponsePersistence(c.persistSyncResponse)
c.engine = engine c.engine = engine
c.engineMutex.Unlock() c.engineMutex.Unlock()
@@ -318,15 +327,6 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
return wrapErr(err) return wrapErr(err)
} }
if loginResp.PeerConfig != nil && loginResp.PeerConfig.AutoUpdate != nil {
// 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()) log.Infof("Netbird engine started, the IP is: %s", peerConfig.GetAddress())
state.Set(StatusConnected) state.Set(StatusConnected)

View File

@@ -27,7 +27,7 @@ import (
"github.com/netbirdio/netbird/client/anonymize" "github.com/netbirdio/netbird/client/anonymize"
"github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/updatemanager/installer" "github.com/netbirdio/netbird/client/internal/updater/installer"
nbstatus "github.com/netbirdio/netbird/client/status" nbstatus "github.com/netbirdio/netbird/client/status"
mgmProto "github.com/netbirdio/netbird/shared/management/proto" mgmProto "github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/util"

View File

@@ -51,7 +51,7 @@ import (
"github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/routemanager"
"github.com/netbirdio/netbird/client/internal/routemanager/systemops" "github.com/netbirdio/netbird/client/internal/routemanager/systemops"
"github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/client/internal/statemanager"
"github.com/netbirdio/netbird/client/internal/updatemanager" "github.com/netbirdio/netbird/client/internal/updater"
"github.com/netbirdio/netbird/client/jobexec" "github.com/netbirdio/netbird/client/jobexec"
cProto "github.com/netbirdio/netbird/client/proto" cProto "github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/client/system"
@@ -79,7 +79,6 @@ const (
var ErrResetConnection = fmt.Errorf("reset connection") var ErrResetConnection = fmt.Errorf("reset connection")
// EngineConfig is a config for the Engine
type EngineConfig struct { type EngineConfig struct {
WgPort int WgPort int
WgIfaceName string WgIfaceName string
@@ -141,6 +140,17 @@ type EngineConfig struct {
LogPath string LogPath string
} }
// EngineServices holds the external service dependencies required by the Engine.
type EngineServices struct {
SignalClient signal.Client
MgmClient mgm.Client
RelayManager *relayClient.Manager
StatusRecorder *peer.Status
Checks []*mgmProto.Checks
StateManager *statemanager.Manager
UpdateManager *updater.Manager
}
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers. // Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
type Engine struct { type Engine struct {
// signal is a Signal Service client // signal is a Signal Service client
@@ -209,7 +219,7 @@ type Engine struct {
flowManager nftypes.FlowManager flowManager nftypes.FlowManager
// auto-update // auto-update
updateManager *updatemanager.Manager updateManager *updater.Manager
// WireGuard interface monitor // WireGuard interface monitor
wgIfaceMonitor *WGIfaceMonitor wgIfaceMonitor *WGIfaceMonitor
@@ -239,22 +249,17 @@ type localIpUpdater interface {
func NewEngine( func NewEngine(
clientCtx context.Context, clientCtx context.Context,
clientCancel context.CancelFunc, clientCancel context.CancelFunc,
signalClient signal.Client,
mgmClient mgm.Client,
relayManager *relayClient.Manager,
config *EngineConfig, config *EngineConfig,
services EngineServices,
mobileDep MobileDependency, mobileDep MobileDependency,
statusRecorder *peer.Status,
checks []*mgmProto.Checks,
stateManager *statemanager.Manager,
) *Engine { ) *Engine {
engine := &Engine{ engine := &Engine{
clientCtx: clientCtx, clientCtx: clientCtx,
clientCancel: clientCancel, clientCancel: clientCancel,
signal: signalClient, signal: services.SignalClient,
signaler: peer.NewSignaler(signalClient, config.WgPrivateKey), signaler: peer.NewSignaler(services.SignalClient, config.WgPrivateKey),
mgmClient: mgmClient, mgmClient: services.MgmClient,
relayManager: relayManager, relayManager: services.RelayManager,
peerStore: peerstore.NewConnStore(), peerStore: peerstore.NewConnStore(),
syncMsgMux: &sync.Mutex{}, syncMsgMux: &sync.Mutex{},
config: config, config: config,
@@ -262,11 +267,12 @@ func NewEngine(
STUNs: []*stun.URI{}, STUNs: []*stun.URI{},
TURNs: []*stun.URI{}, TURNs: []*stun.URI{},
networkSerial: 0, networkSerial: 0,
statusRecorder: statusRecorder, statusRecorder: services.StatusRecorder,
stateManager: stateManager, stateManager: services.StateManager,
checks: checks, checks: services.Checks,
probeStunTurn: relay.NewStunTurnProbe(relay.DefaultCacheTTL), probeStunTurn: relay.NewStunTurnProbe(relay.DefaultCacheTTL),
jobExecutor: jobexec.NewExecutor(), jobExecutor: jobexec.NewExecutor(),
updateManager: services.UpdateManager,
} }
log.Infof("I am: %s", config.WgPrivateKey.PublicKey().String()) log.Infof("I am: %s", config.WgPrivateKey.PublicKey().String())
@@ -309,7 +315,7 @@ func (e *Engine) Stop() error {
} }
if e.updateManager != nil { if e.updateManager != nil {
e.updateManager.Stop() e.updateManager.SetDownloadOnly()
} }
log.Info("cleaning up status recorder states") log.Info("cleaning up status recorder states")
@@ -559,13 +565,6 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
return nil return nil
} }
func (e *Engine) InitialUpdateHandling(autoUpdateSettings *mgmProto.AutoUpdateSettings) {
e.syncMsgMux.Lock()
defer e.syncMsgMux.Unlock()
e.handleAutoUpdateVersion(autoUpdateSettings, true)
}
func (e *Engine) createFirewall() error { func (e *Engine) createFirewall() error {
if e.config.DisableFirewall { if e.config.DisableFirewall {
log.Infof("firewall is disabled") log.Infof("firewall is disabled")
@@ -793,39 +792,22 @@ func (e *Engine) PopulateNetbirdConfig(netbirdConfig *mgmProto.NetbirdConfig, mg
return nil return nil
} }
func (e *Engine) handleAutoUpdateVersion(autoUpdateSettings *mgmProto.AutoUpdateSettings, initialCheck bool) { func (e *Engine) handleAutoUpdateVersion(autoUpdateSettings *mgmProto.AutoUpdateSettings) {
if e.updateManager == nil {
return
}
if autoUpdateSettings == nil { if autoUpdateSettings == nil {
return return
} }
disabled := autoUpdateSettings.Version == disableAutoUpdate if autoUpdateSettings.Version == disableAutoUpdate {
log.Infof("auto-update is disabled")
// stop and cleanup if disabled e.updateManager.SetDownloadOnly()
if e.updateManager != nil && disabled {
log.Infof("auto-update is disabled, stopping update manager")
e.updateManager.Stop()
e.updateManager = nil
return return
} }
// Skip check unless AlwaysUpdate is enabled or this is the initial check at startup e.updateManager.SetVersion(autoUpdateSettings.Version, autoUpdateSettings.AlwaysUpdate)
if !autoUpdateSettings.AlwaysUpdate && !initialCheck {
log.Debugf("skipping auto-update check, AlwaysUpdate is false and this is not the initial check")
return
}
// Start manager if needed
if e.updateManager == nil {
log.Infof("starting auto-update manager")
updateManager, err := updatemanager.NewManager(e.statusRecorder, e.stateManager)
if err != nil {
return
}
e.updateManager = updateManager
e.updateManager.Start(e.ctx)
}
log.Infof("handling auto-update version: %s", autoUpdateSettings.Version)
e.updateManager.SetVersion(autoUpdateSettings.Version)
} }
func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
@@ -842,7 +824,7 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
} }
if update.NetworkMap != nil && update.NetworkMap.PeerConfig != nil { if update.NetworkMap != nil && update.NetworkMap.PeerConfig != nil {
e.handleAutoUpdateVersion(update.NetworkMap.PeerConfig.AutoUpdate, false) e.handleAutoUpdateVersion(update.NetworkMap.PeerConfig.AutoUpdate)
} }
if update.GetNetbirdConfig() != nil { if update.GetNetbirdConfig() != nil {

View File

@@ -251,9 +251,6 @@ func TestEngine_SSH(t *testing.T) {
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU) relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
engine := NewEngine( engine := NewEngine(
ctx, cancel, ctx, cancel,
&signal.MockClient{},
&mgmt.MockClient{},
relayMgr,
&EngineConfig{ &EngineConfig{
WgIfaceName: "utun101", WgIfaceName: "utun101",
WgAddr: "100.64.0.1/24", WgAddr: "100.64.0.1/24",
@@ -263,10 +260,13 @@ func TestEngine_SSH(t *testing.T) {
MTU: iface.DefaultMTU, MTU: iface.DefaultMTU,
SSHKey: sshKey, SSHKey: sshKey,
}, },
EngineServices{
SignalClient: &signal.MockClient{},
MgmClient: &mgmt.MockClient{},
RelayManager: relayMgr,
StatusRecorder: peer.NewRecorder("https://mgm"),
},
MobileDependency{}, MobileDependency{},
peer.NewRecorder("https://mgm"),
nil,
nil,
) )
engine.dnsServer = &dns.MockServer{ engine.dnsServer = &dns.MockServer{
@@ -428,13 +428,18 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
defer cancel() defer cancel()
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU) relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, relayMgr, &EngineConfig{ engine := NewEngine(ctx, cancel, &EngineConfig{
WgIfaceName: "utun102", WgIfaceName: "utun102",
WgAddr: "100.64.0.1/24", WgAddr: "100.64.0.1/24",
WgPrivateKey: key, WgPrivateKey: key,
WgPort: 33100, WgPort: 33100,
MTU: iface.DefaultMTU, MTU: iface.DefaultMTU,
}, MobileDependency{}, peer.NewRecorder("https://mgm"), nil, nil) }, EngineServices{
SignalClient: &signal.MockClient{},
MgmClient: &mgmt.MockClient{},
RelayManager: relayMgr,
StatusRecorder: peer.NewRecorder("https://mgm"),
}, MobileDependency{})
wgIface := &MockWGIface{ wgIface := &MockWGIface{
NameFunc: func() string { return "utun102" }, NameFunc: func() string { return "utun102" },
@@ -647,13 +652,18 @@ func TestEngine_Sync(t *testing.T) {
return nil return nil
} }
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU) relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{SyncFunc: syncFunc}, relayMgr, &EngineConfig{ engine := NewEngine(ctx, cancel, &EngineConfig{
WgIfaceName: "utun103", WgIfaceName: "utun103",
WgAddr: "100.64.0.1/24", WgAddr: "100.64.0.1/24",
WgPrivateKey: key, WgPrivateKey: key,
WgPort: 33100, WgPort: 33100,
MTU: iface.DefaultMTU, MTU: iface.DefaultMTU,
}, MobileDependency{}, peer.NewRecorder("https://mgm"), nil, nil) }, EngineServices{
SignalClient: &signal.MockClient{},
MgmClient: &mgmt.MockClient{SyncFunc: syncFunc},
RelayManager: relayMgr,
StatusRecorder: peer.NewRecorder("https://mgm"),
}, MobileDependency{})
engine.ctx = ctx engine.ctx = ctx
engine.dnsServer = &dns.MockServer{ engine.dnsServer = &dns.MockServer{
@@ -812,13 +822,18 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
wgAddr := fmt.Sprintf("100.66.%d.1/24", n) wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU) relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, relayMgr, &EngineConfig{ engine := NewEngine(ctx, cancel, &EngineConfig{
WgIfaceName: wgIfaceName, WgIfaceName: wgIfaceName,
WgAddr: wgAddr, WgAddr: wgAddr,
WgPrivateKey: key, WgPrivateKey: key,
WgPort: 33100, WgPort: 33100,
MTU: iface.DefaultMTU, MTU: iface.DefaultMTU,
}, MobileDependency{}, peer.NewRecorder("https://mgm"), nil, nil) }, EngineServices{
SignalClient: &signal.MockClient{},
MgmClient: &mgmt.MockClient{},
RelayManager: relayMgr,
StatusRecorder: peer.NewRecorder("https://mgm"),
}, MobileDependency{})
engine.ctx = ctx engine.ctx = ctx
newNet, err := stdnet.NewNet(context.Background(), nil) newNet, err := stdnet.NewNet(context.Background(), nil)
if err != nil { if err != nil {
@@ -1014,13 +1029,18 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
wgAddr := fmt.Sprintf("100.66.%d.1/24", n) wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU) relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, relayMgr, &EngineConfig{ engine := NewEngine(ctx, cancel, &EngineConfig{
WgIfaceName: wgIfaceName, WgIfaceName: wgIfaceName,
WgAddr: wgAddr, WgAddr: wgAddr,
WgPrivateKey: key, WgPrivateKey: key,
WgPort: 33100, WgPort: 33100,
MTU: iface.DefaultMTU, MTU: iface.DefaultMTU,
}, MobileDependency{}, peer.NewRecorder("https://mgm"), nil, nil) }, EngineServices{
SignalClient: &signal.MockClient{},
MgmClient: &mgmt.MockClient{},
RelayManager: relayMgr,
StatusRecorder: peer.NewRecorder("https://mgm"),
}, MobileDependency{})
engine.ctx = ctx engine.ctx = ctx
newNet, err := stdnet.NewNet(context.Background(), nil) newNet, err := stdnet.NewNet(context.Background(), nil)
@@ -1546,7 +1566,12 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin
} }
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU) relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
e, err := NewEngine(ctx, cancel, signalClient, mgmtClient, relayMgr, conf, MobileDependency{}, peer.NewRecorder("https://mgm"), nil, nil), nil e, err := NewEngine(ctx, cancel, conf, EngineServices{
SignalClient: signalClient,
MgmClient: mgmtClient,
RelayManager: relayMgr,
StatusRecorder: peer.NewRecorder("https://mgm"),
}, MobileDependency{}), nil
e.ctx = ctx e.ctx = ctx
return e, err return e, err
} }

View File

@@ -1,214 +0,0 @@
//go:build windows || darwin
package updatemanager
import (
"context"
"fmt"
"path"
"testing"
"time"
v "github.com/hashicorp/go-version"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/statemanager"
)
type versionUpdateMock struct {
latestVersion *v.Version
onUpdate func()
}
func (v versionUpdateMock) StopWatch() {}
func (v versionUpdateMock) SetDaemonVersion(newVersion string) bool {
return false
}
func (v *versionUpdateMock) SetOnUpdateListener(updateFn func()) {
v.onUpdate = updateFn
}
func (v versionUpdateMock) LatestVersion() *v.Version {
return v.latestVersion
}
func (v versionUpdateMock) StartFetcher() {}
func Test_LatestVersion(t *testing.T) {
testMatrix := []struct {
name string
daemonVersion string
initialLatestVersion *v.Version
latestVersion *v.Version
shouldUpdateInit bool
shouldUpdateLater bool
}{
{
name: "Should only trigger update once due to time between triggers being < 5 Minutes",
daemonVersion: "1.0.0",
initialLatestVersion: v.Must(v.NewSemver("1.0.1")),
latestVersion: v.Must(v.NewSemver("1.0.2")),
shouldUpdateInit: true,
shouldUpdateLater: false,
},
{
name: "Shouldn't update initially, but should update as soon as latest version is fetched",
daemonVersion: "1.0.0",
initialLatestVersion: nil,
latestVersion: v.Must(v.NewSemver("1.0.1")),
shouldUpdateInit: false,
shouldUpdateLater: true,
},
}
for idx, c := range testMatrix {
mockUpdate := &versionUpdateMock{latestVersion: c.initialLatestVersion}
tmpFile := path.Join(t.TempDir(), fmt.Sprintf("update-test-%d.json", idx))
m, _ := newManager(peer.NewRecorder(""), statemanager.New(tmpFile))
m.update = mockUpdate
targetVersionChan := make(chan string, 1)
m.triggerUpdateFn = func(ctx context.Context, targetVersion string) error {
targetVersionChan <- targetVersion
return nil
}
m.currentVersion = c.daemonVersion
m.Start(context.Background())
m.SetVersion("latest")
var triggeredInit bool
select {
case targetVersion := <-targetVersionChan:
if targetVersion != c.initialLatestVersion.String() {
t.Errorf("%s: Initial update version mismatch, expected %v, got %v", c.name, c.initialLatestVersion.String(), targetVersion)
}
triggeredInit = true
case <-time.After(10 * time.Millisecond):
triggeredInit = false
}
if triggeredInit != c.shouldUpdateInit {
t.Errorf("%s: Initial update trigger mismatch, expected %v, got %v", c.name, c.shouldUpdateInit, triggeredInit)
}
mockUpdate.latestVersion = c.latestVersion
mockUpdate.onUpdate()
var triggeredLater bool
select {
case targetVersion := <-targetVersionChan:
if targetVersion != c.latestVersion.String() {
t.Errorf("%s: Update version mismatch, expected %v, got %v", c.name, c.latestVersion.String(), targetVersion)
}
triggeredLater = true
case <-time.After(10 * time.Millisecond):
triggeredLater = false
}
if triggeredLater != c.shouldUpdateLater {
t.Errorf("%s: Update trigger mismatch, expected %v, got %v", c.name, c.shouldUpdateLater, triggeredLater)
}
m.Stop()
}
}
func Test_HandleUpdate(t *testing.T) {
testMatrix := []struct {
name string
daemonVersion string
latestVersion *v.Version
expectedVersion string
shouldUpdate bool
}{
{
name: "Update to a specific version should update regardless of if latestVersion is available yet",
daemonVersion: "0.55.0",
latestVersion: nil,
expectedVersion: "0.56.0",
shouldUpdate: true,
},
{
name: "Update to specific version should not update if version matches",
daemonVersion: "0.55.0",
latestVersion: nil,
expectedVersion: "0.55.0",
shouldUpdate: false,
},
{
name: "Update to specific version should not update if current version is newer",
daemonVersion: "0.55.0",
latestVersion: nil,
expectedVersion: "0.54.0",
shouldUpdate: false,
},
{
name: "Update to latest version should update if latest is newer",
daemonVersion: "0.55.0",
latestVersion: v.Must(v.NewSemver("0.56.0")),
expectedVersion: "latest",
shouldUpdate: true,
},
{
name: "Update to latest version should not update if latest == current",
daemonVersion: "0.56.0",
latestVersion: v.Must(v.NewSemver("0.56.0")),
expectedVersion: "latest",
shouldUpdate: false,
},
{
name: "Should not update if daemon version is invalid",
daemonVersion: "development",
latestVersion: v.Must(v.NewSemver("1.0.0")),
expectedVersion: "latest",
shouldUpdate: false,
},
{
name: "Should not update if expecting latest and latest version is unavailable",
daemonVersion: "0.55.0",
latestVersion: nil,
expectedVersion: "latest",
shouldUpdate: false,
},
{
name: "Should not update if expected version is invalid",
daemonVersion: "0.55.0",
latestVersion: nil,
expectedVersion: "development",
shouldUpdate: false,
},
}
for idx, c := range testMatrix {
tmpFile := path.Join(t.TempDir(), fmt.Sprintf("update-test-%d.json", idx))
m, _ := newManager(peer.NewRecorder(""), statemanager.New(tmpFile))
m.update = &versionUpdateMock{latestVersion: c.latestVersion}
targetVersionChan := make(chan string, 1)
m.triggerUpdateFn = func(ctx context.Context, targetVersion string) error {
targetVersionChan <- targetVersion
return nil
}
m.currentVersion = c.daemonVersion
m.Start(context.Background())
m.SetVersion(c.expectedVersion)
var updateTriggered bool
select {
case targetVersion := <-targetVersionChan:
if c.expectedVersion == "latest" && targetVersion != c.latestVersion.String() {
t.Errorf("%s: Update version mismatch, expected %v, got %v", c.name, c.latestVersion.String(), targetVersion)
} else if c.expectedVersion != "latest" && targetVersion != c.expectedVersion {
t.Errorf("%s: Update version mismatch, expected %v, got %v", c.name, c.expectedVersion, targetVersion)
}
updateTriggered = true
case <-time.After(10 * time.Millisecond):
updateTriggered = false
}
if updateTriggered != c.shouldUpdate {
t.Errorf("%s: Update trigger mismatch, expected %v, got %v", c.name, c.shouldUpdate, updateTriggered)
}
m.Stop()
}
}

View File

@@ -1,39 +0,0 @@
//go:build !windows && !darwin
package updatemanager
import (
"context"
"fmt"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/statemanager"
)
// Manager is a no-op stub for unsupported platforms
type Manager struct{}
// NewManager returns a no-op manager for unsupported platforms
func NewManager(statusRecorder *peer.Status, stateManager *statemanager.Manager) (*Manager, error) {
return nil, fmt.Errorf("update manager is not supported on this platform")
}
// CheckUpdateSuccess is a no-op on unsupported platforms
func (m *Manager) CheckUpdateSuccess(ctx context.Context) {
// no-op
}
// Start is a no-op on unsupported platforms
func (m *Manager) Start(ctx context.Context) {
// no-op
}
// SetVersion is a no-op on unsupported platforms
func (m *Manager) SetVersion(expectedVersion string) {
// no-op
}
// Stop is a no-op on unsupported platforms
func (m *Manager) Stop() {
// no-op
}

View File

@@ -1,4 +1,4 @@
// Package updatemanager provides automatic update management for the NetBird client. // Package updater provides automatic update management for the NetBird client.
// It monitors for new versions, handles update triggers from management server directives, // It monitors for new versions, handles update triggers from management server directives,
// and orchestrates the download and installation of client updates. // and orchestrates the download and installation of client updates.
// //
@@ -32,4 +32,4 @@
// //
// This enables verification of successful updates and appropriate user notification // This enables verification of successful updates and appropriate user notification
// after the client restarts with the new version. // after the client restarts with the new version.
package updatemanager package updater

View File

@@ -16,8 +16,8 @@ import (
goversion "github.com/hashicorp/go-version" goversion "github.com/hashicorp/go-version"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/updatemanager/downloader" "github.com/netbirdio/netbird/client/internal/updater/downloader"
"github.com/netbirdio/netbird/client/internal/updatemanager/reposign" "github.com/netbirdio/netbird/client/internal/updater/reposign"
) )
type Installer struct { type Installer struct {

View File

@@ -203,7 +203,10 @@ func (rh *ResultHandler) write(result Result) error {
func (rh *ResultHandler) cleanup() error { func (rh *ResultHandler) cleanup() error {
err := os.Remove(rh.resultFile) err := os.Remove(rh.resultFile)
if err != nil && !os.IsNotExist(err) { if err != nil {
if os.IsNotExist(err) {
return nil
}
return err return err
} }
log.Debugf("delete installer result file: %s", rh.resultFile) log.Debugf("delete installer result file: %s", rh.resultFile)

View File

@@ -1,12 +1,9 @@
//go:build windows || darwin package updater
package updatemanager
import ( import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"runtime"
"sync" "sync"
"time" "time"
@@ -15,7 +12,7 @@ import (
"github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/client/internal/statemanager"
"github.com/netbirdio/netbird/client/internal/updatemanager/installer" "github.com/netbirdio/netbird/client/internal/updater/installer"
cProto "github.com/netbirdio/netbird/client/proto" cProto "github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/version" "github.com/netbirdio/netbird/version"
) )
@@ -41,6 +38,9 @@ type Manager struct {
statusRecorder *peer.Status statusRecorder *peer.Status
stateManager *statemanager.Manager stateManager *statemanager.Manager
downloadOnly bool // true when no enforcement from management; notifies UI to download latest
forceUpdate bool // true when management sets AlwaysUpdate; skips UI interaction and installs directly
lastTrigger time.Time lastTrigger time.Time
mgmUpdateChan chan struct{} mgmUpdateChan chan struct{}
updateChannel chan struct{} updateChannel chan struct{}
@@ -53,24 +53,24 @@ type Manager struct {
expectedVersion *v.Version expectedVersion *v.Version
updateToLatestVersion bool updateToLatestVersion bool
// updateMutex protect update and expectedVersion fields pendingVersion *v.Version
// updateMutex protects update, expectedVersion, updateToLatestVersion,
// downloadOnly, forceUpdate, pendingVersion, and lastTrigger fields
updateMutex sync.Mutex updateMutex sync.Mutex
triggerUpdateFn func(context.Context, string) error // installMutex and installing guard against concurrent installation attempts
installMutex sync.Mutex
installing bool
// protect to start the service multiple times
mu sync.Mutex
autoUpdateSupported func() bool
} }
func NewManager(statusRecorder *peer.Status, stateManager *statemanager.Manager) (*Manager, error) { // NewManager creates a new update manager. The manager is single-use: once Stop() is called, it cannot be restarted.
if runtime.GOOS == "darwin" { func NewManager(statusRecorder *peer.Status, stateManager *statemanager.Manager) *Manager {
isBrew := !installer.TypeOfInstaller(context.Background()).Downloadable()
if isBrew {
log.Warnf("auto-update disabled on Home Brew installation")
return nil, fmt.Errorf("auto-update not supported on Home Brew installation yet")
}
}
return newManager(statusRecorder, stateManager)
}
func newManager(statusRecorder *peer.Status, stateManager *statemanager.Manager) (*Manager, error) {
manager := &Manager{ manager := &Manager{
statusRecorder: statusRecorder, statusRecorder: statusRecorder,
stateManager: stateManager, stateManager: stateManager,
@@ -78,12 +78,13 @@ func newManager(statusRecorder *peer.Status, stateManager *statemanager.Manager)
updateChannel: make(chan struct{}, 1), updateChannel: make(chan struct{}, 1),
currentVersion: version.NetbirdVersion(), currentVersion: version.NetbirdVersion(),
update: version.NewUpdate("nb/client"), update: version.NewUpdate("nb/client"),
downloadOnly: true,
autoUpdateSupported: isAutoUpdateSupported,
} }
manager.triggerUpdateFn = manager.triggerUpdate
stateManager.RegisterState(&UpdateState{}) stateManager.RegisterState(&UpdateState{})
return manager, nil return manager
} }
// CheckUpdateSuccess checks if the update was successful and send a notification. // CheckUpdateSuccess checks if the update was successful and send a notification.
@@ -124,8 +125,10 @@ func (m *Manager) CheckUpdateSuccess(ctx context.Context) {
} }
func (m *Manager) Start(ctx context.Context) { func (m *Manager) Start(ctx context.Context) {
log.Infof("starting update manager")
m.mu.Lock()
defer m.mu.Unlock()
if m.cancel != nil { if m.cancel != nil {
log.Errorf("Manager already started")
return return
} }
@@ -142,13 +145,32 @@ func (m *Manager) Start(ctx context.Context) {
m.cancel = cancel m.cancel = cancel
m.wg.Add(1) m.wg.Add(1)
go m.updateLoop(ctx) go func() {
defer m.wg.Done()
m.updateLoop(ctx)
}()
} }
func (m *Manager) SetVersion(expectedVersion string) { func (m *Manager) SetDownloadOnly() {
log.Infof("set expected agent version for upgrade: %s", expectedVersion) m.updateMutex.Lock()
if m.cancel == nil { m.downloadOnly = true
log.Errorf("manager not started") m.forceUpdate = false
m.expectedVersion = nil
m.updateToLatestVersion = false
m.lastTrigger = time.Time{}
m.updateMutex.Unlock()
select {
case m.mgmUpdateChan <- struct{}{}:
default:
}
}
func (m *Manager) SetVersion(expectedVersion string, forceUpdate bool) {
log.Infof("expected version changed to %s, force update: %t", expectedVersion, forceUpdate)
if !m.autoUpdateSupported() {
log.Warnf("auto-update not supported on this platform")
return return
} }
@@ -159,6 +181,7 @@ func (m *Manager) SetVersion(expectedVersion string) {
log.Errorf("empty expected version provided") log.Errorf("empty expected version provided")
m.expectedVersion = nil m.expectedVersion = nil
m.updateToLatestVersion = false m.updateToLatestVersion = false
m.downloadOnly = true
return return
} }
@@ -178,12 +201,97 @@ func (m *Manager) SetVersion(expectedVersion string) {
m.updateToLatestVersion = false m.updateToLatestVersion = false
} }
m.lastTrigger = time.Time{}
m.downloadOnly = false
m.forceUpdate = forceUpdate
select { select {
case m.mgmUpdateChan <- struct{}{}: case m.mgmUpdateChan <- struct{}{}:
default: default:
} }
} }
// Install triggers the installation of the pending version. It is called when the user clicks the install button in the UI.
func (m *Manager) Install(ctx context.Context) error {
if !m.autoUpdateSupported() {
return fmt.Errorf("auto-update not supported on this platform")
}
m.updateMutex.Lock()
pending := m.pendingVersion
m.updateMutex.Unlock()
if pending == nil {
return fmt.Errorf("no pending version to install")
}
return m.tryInstall(ctx, pending)
}
// tryInstall ensures only one installation runs at a time. Concurrent callers
// receive an error immediately rather than queuing behind a running install.
func (m *Manager) tryInstall(ctx context.Context, targetVersion *v.Version) error {
m.installMutex.Lock()
if m.installing {
m.installMutex.Unlock()
return fmt.Errorf("installation already in progress")
}
m.installing = true
m.installMutex.Unlock()
defer func() {
m.installMutex.Lock()
m.installing = false
m.installMutex.Unlock()
}()
return m.install(ctx, targetVersion)
}
// NotifyUI re-publishes the current update state to a newly connected UI client.
// Only needed for download-only mode where the latest version is already cached
// NotifyUI re-publishes the current update state so a newly connected UI gets the info.
func (m *Manager) NotifyUI() {
m.updateMutex.Lock()
if m.update == nil {
m.updateMutex.Unlock()
return
}
downloadOnly := m.downloadOnly
pendingVersion := m.pendingVersion
latestVersion := m.update.LatestVersion()
m.updateMutex.Unlock()
if downloadOnly {
if latestVersion == nil {
return
}
currentVersion, err := v.NewVersion(m.currentVersion)
if err != nil || currentVersion.GreaterThanOrEqual(latestVersion) {
return
}
m.statusRecorder.PublishEvent(
cProto.SystemEvent_INFO,
cProto.SystemEvent_SYSTEM,
"New version available",
"",
map[string]string{"new_version_available": latestVersion.String()},
)
return
}
if pendingVersion != nil {
m.statusRecorder.PublishEvent(
cProto.SystemEvent_INFO,
cProto.SystemEvent_SYSTEM,
"New version available",
"",
map[string]string{"new_version_available": pendingVersion.String(), "enforced": "true"},
)
}
}
// Stop is not used at the moment because it fully depends on the daemon. In a future refactor it may make sense to use it.
func (m *Manager) Stop() { func (m *Manager) Stop() {
if m.cancel == nil { if m.cancel == nil {
return return
@@ -214,8 +322,6 @@ func (m *Manager) onContextCancel() {
} }
func (m *Manager) updateLoop(ctx context.Context) { func (m *Manager) updateLoop(ctx context.Context) {
defer m.wg.Done()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -239,55 +345,89 @@ func (m *Manager) handleUpdate(ctx context.Context) {
return return
} }
expectedVersion := m.expectedVersion downloadOnly := m.downloadOnly
useLatest := m.updateToLatestVersion forceUpdate := m.forceUpdate
curLatestVersion := m.update.LatestVersion() curLatestVersion := m.update.LatestVersion()
m.updateMutex.Unlock()
switch { switch {
// Resolve "latest" to actual version // Download-only mode or resolve "latest" to actual version
case useLatest: case downloadOnly, m.updateToLatestVersion:
if curLatestVersion == nil { if curLatestVersion == nil {
log.Tracef("latest version not fetched yet") log.Tracef("latest version not fetched yet")
m.updateMutex.Unlock()
return return
} }
updateVersion = curLatestVersion updateVersion = curLatestVersion
// Update to specific version // Install to specific version
case expectedVersion != nil: case m.expectedVersion != nil:
updateVersion = expectedVersion updateVersion = m.expectedVersion
default: default:
log.Debugf("no expected version information set") log.Debugf("no expected version information set")
m.updateMutex.Unlock()
return return
} }
log.Debugf("checking update option, current version: %s, target version: %s", m.currentVersion, updateVersion) log.Debugf("checking update option, current version: %s, target version: %s", m.currentVersion, updateVersion)
if !m.shouldUpdate(updateVersion) { if !m.shouldUpdate(updateVersion, forceUpdate) {
m.updateMutex.Unlock()
return return
} }
m.lastTrigger = time.Now() m.lastTrigger = time.Now()
log.Infof("Auto-update triggered, current version: %s, target version: %s", m.currentVersion, updateVersion) log.Infof("new version available: %s", updateVersion)
if !downloadOnly && !forceUpdate {
m.pendingVersion = updateVersion
}
m.updateMutex.Unlock()
if downloadOnly {
m.statusRecorder.PublishEvent( m.statusRecorder.PublishEvent(
cProto.SystemEvent_CRITICAL, cProto.SystemEvent_INFO,
cProto.SystemEvent_SYSTEM, cProto.SystemEvent_SYSTEM,
"Automatically updating client", "New version available",
"Your client version is older than auto-update version set in Management, updating client now.", "",
nil, map[string]string{"new_version_available": updateVersion.String()},
) )
return
}
if forceUpdate {
if err := m.tryInstall(ctx, updateVersion); err != nil {
log.Errorf("force update failed: %v", err)
}
return
}
m.statusRecorder.PublishEvent(
cProto.SystemEvent_INFO,
cProto.SystemEvent_SYSTEM,
"New version available",
"",
map[string]string{"new_version_available": updateVersion.String(), "enforced": "true"},
)
}
func (m *Manager) install(ctx context.Context, pendingVersion *v.Version) error {
m.statusRecorder.PublishEvent(
cProto.SystemEvent_CRITICAL,
cProto.SystemEvent_SYSTEM,
"Updating client",
"Installing update now.",
nil,
)
m.statusRecorder.PublishEvent( m.statusRecorder.PublishEvent(
cProto.SystemEvent_CRITICAL, cProto.SystemEvent_CRITICAL,
cProto.SystemEvent_SYSTEM, cProto.SystemEvent_SYSTEM,
"", "",
"", "",
map[string]string{"progress_window": "show", "version": updateVersion.String()}, map[string]string{"progress_window": "show", "version": pendingVersion.String()},
) )
updateState := UpdateState{ updateState := UpdateState{
PreUpdateVersion: m.currentVersion, PreUpdateVersion: m.currentVersion,
TargetVersion: updateVersion.String(), TargetVersion: pendingVersion.String(),
} }
if err := m.stateManager.UpdateState(updateState); err != nil { if err := m.stateManager.UpdateState(updateState); err != nil {
log.Warnf("failed to update state: %v", err) log.Warnf("failed to update state: %v", err)
} else { } else {
@@ -296,8 +436,9 @@ func (m *Manager) handleUpdate(ctx context.Context) {
} }
} }
if err := m.triggerUpdateFn(ctx, updateVersion.String()); err != nil { inst := installer.New()
log.Errorf("Error triggering auto-update: %v", err) if err := inst.RunInstallation(ctx, pendingVersion.String()); err != nil {
log.Errorf("error triggering update: %v", err)
m.statusRecorder.PublishEvent( m.statusRecorder.PublishEvent(
cProto.SystemEvent_ERROR, cProto.SystemEvent_ERROR,
cProto.SystemEvent_SYSTEM, cProto.SystemEvent_SYSTEM,
@@ -305,7 +446,9 @@ func (m *Manager) handleUpdate(ctx context.Context) {
fmt.Sprintf("Auto-update failed: %v", err), fmt.Sprintf("Auto-update failed: %v", err),
nil, nil,
) )
return err
} }
return nil
} }
// loadAndDeleteUpdateState loads the update state, deletes it from storage, and returns it. // loadAndDeleteUpdateState loads the update state, deletes it from storage, and returns it.
@@ -339,7 +482,7 @@ func (m *Manager) loadAndDeleteUpdateState(ctx context.Context) (*UpdateState, e
return updateState, nil return updateState, nil
} }
func (m *Manager) shouldUpdate(updateVersion *v.Version) bool { func (m *Manager) shouldUpdate(updateVersion *v.Version, forceUpdate bool) bool {
if m.currentVersion == developmentVersion { if m.currentVersion == developmentVersion {
log.Debugf("skipping auto-update, running development version") log.Debugf("skipping auto-update, running development version")
return false return false
@@ -354,8 +497,8 @@ func (m *Manager) shouldUpdate(updateVersion *v.Version) bool {
return false return false
} }
if time.Since(m.lastTrigger) < 5*time.Minute { if forceUpdate && time.Since(m.lastTrigger) < 3*time.Minute {
log.Debugf("skipping auto-update, last update was %s ago", time.Since(m.lastTrigger)) log.Infof("skipping auto-update, last update was %s ago", time.Since(m.lastTrigger))
return false return false
} }
@@ -367,8 +510,3 @@ func (m *Manager) lastResultErrReason() string {
result := installer.NewResultHandler(inst.TempDir()) result := installer.NewResultHandler(inst.TempDir())
return result.GetErrorResultReason() return result.GetErrorResultReason()
} }
func (m *Manager) triggerUpdate(ctx context.Context, targetVersion string) error {
inst := installer.New()
return inst.RunInstallation(ctx, targetVersion)
}

View File

@@ -0,0 +1,111 @@
//go:build !windows && !darwin
package updater
import (
"context"
"fmt"
"path"
"testing"
"time"
v "github.com/hashicorp/go-version"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/statemanager"
)
// On Linux, only Mode 1 (downloadOnly) is supported.
// SetVersion is a no-op because auto-update installation is not supported.
func Test_LatestVersion_Linux(t *testing.T) {
testMatrix := []struct {
name string
daemonVersion string
initialLatestVersion *v.Version
latestVersion *v.Version
shouldUpdateInit bool
shouldUpdateLater bool
}{
{
name: "Should notify again when a newer version arrives even within 5 minutes",
daemonVersion: "1.0.0",
initialLatestVersion: v.Must(v.NewSemver("1.0.1")),
latestVersion: v.Must(v.NewSemver("1.0.2")),
shouldUpdateInit: true,
shouldUpdateLater: true,
},
{
name: "Shouldn't notify initially, but should notify as soon as latest version is fetched",
daemonVersion: "1.0.0",
initialLatestVersion: nil,
latestVersion: v.Must(v.NewSemver("1.0.1")),
shouldUpdateInit: false,
shouldUpdateLater: true,
},
}
for idx, c := range testMatrix {
mockUpdate := &versionUpdateMock{latestVersion: c.initialLatestVersion}
tmpFile := path.Join(t.TempDir(), fmt.Sprintf("update-test-%d.json", idx))
recorder := peer.NewRecorder("")
sub := recorder.SubscribeToEvents()
defer recorder.UnsubscribeFromEvents(sub)
m := NewManager(recorder, statemanager.New(tmpFile))
m.update = mockUpdate
m.currentVersion = c.daemonVersion
m.Start(context.Background())
m.SetDownloadOnly()
ver, enforced := waitForUpdateEvent(sub, 500*time.Millisecond)
triggeredInit := ver != ""
if enforced {
t.Errorf("%s: Linux Mode 1 must never have enforced metadata", c.name)
}
if triggeredInit != c.shouldUpdateInit {
t.Errorf("%s: Initial notify mismatch, expected %v, got %v", c.name, c.shouldUpdateInit, triggeredInit)
}
if triggeredInit && c.initialLatestVersion != nil && ver != c.initialLatestVersion.String() {
t.Errorf("%s: Initial version mismatch, expected %v, got %v", c.name, c.initialLatestVersion.String(), ver)
}
mockUpdate.latestVersion = c.latestVersion
mockUpdate.onUpdate()
ver, enforced = waitForUpdateEvent(sub, 500*time.Millisecond)
triggeredLater := ver != ""
if enforced {
t.Errorf("%s: Linux Mode 1 must never have enforced metadata", c.name)
}
if triggeredLater != c.shouldUpdateLater {
t.Errorf("%s: Later notify mismatch, expected %v, got %v", c.name, c.shouldUpdateLater, triggeredLater)
}
if triggeredLater && c.latestVersion != nil && ver != c.latestVersion.String() {
t.Errorf("%s: Later version mismatch, expected %v, got %v", c.name, c.latestVersion.String(), ver)
}
m.Stop()
}
}
func Test_SetVersion_NoOp_Linux(t *testing.T) {
// On Linux, SetVersion should be a no-op — no events fired
tmpFile := path.Join(t.TempDir(), "update-test-noop.json")
recorder := peer.NewRecorder("")
sub := recorder.SubscribeToEvents()
defer recorder.UnsubscribeFromEvents(sub)
m := NewManager(recorder, statemanager.New(tmpFile))
m.update = &versionUpdateMock{latestVersion: v.Must(v.NewSemver("1.0.1"))}
m.currentVersion = "1.0.0"
m.Start(context.Background())
m.SetVersion("1.0.1", false)
ver, _ := waitForUpdateEvent(sub, 500*time.Millisecond)
if ver != "" {
t.Errorf("SetVersion should be a no-op on Linux, but got event with version %s", ver)
}
m.Stop()
}

View File

@@ -0,0 +1,227 @@
//go:build windows || darwin
package updater
import (
"context"
"fmt"
"path"
"testing"
"time"
v "github.com/hashicorp/go-version"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/statemanager"
cProto "github.com/netbirdio/netbird/client/proto"
)
func Test_LatestVersion(t *testing.T) {
testMatrix := []struct {
name string
daemonVersion string
initialLatestVersion *v.Version
latestVersion *v.Version
shouldUpdateInit bool
shouldUpdateLater bool
}{
{
name: "Should notify again when a newer version arrives even within 5 minutes",
daemonVersion: "1.0.0",
initialLatestVersion: v.Must(v.NewSemver("1.0.1")),
latestVersion: v.Must(v.NewSemver("1.0.2")),
shouldUpdateInit: true,
shouldUpdateLater: true,
},
{
name: "Shouldn't update initially, but should update as soon as latest version is fetched",
daemonVersion: "1.0.0",
initialLatestVersion: nil,
latestVersion: v.Must(v.NewSemver("1.0.1")),
shouldUpdateInit: false,
shouldUpdateLater: true,
},
}
for idx, c := range testMatrix {
mockUpdate := &versionUpdateMock{latestVersion: c.initialLatestVersion}
tmpFile := path.Join(t.TempDir(), fmt.Sprintf("update-test-%d.json", idx))
recorder := peer.NewRecorder("")
sub := recorder.SubscribeToEvents()
defer recorder.UnsubscribeFromEvents(sub)
m := NewManager(recorder, statemanager.New(tmpFile))
m.update = mockUpdate
m.currentVersion = c.daemonVersion
m.autoUpdateSupported = func() bool { return true }
m.Start(context.Background())
m.SetVersion("latest", false)
ver, _ := waitForUpdateEvent(sub, 500*time.Millisecond)
triggeredInit := ver != ""
if triggeredInit != c.shouldUpdateInit {
t.Errorf("%s: Initial update trigger mismatch, expected %v, got %v", c.name, c.shouldUpdateInit, triggeredInit)
}
if triggeredInit && c.initialLatestVersion != nil && ver != c.initialLatestVersion.String() {
t.Errorf("%s: Initial update version mismatch, expected %v, got %v", c.name, c.initialLatestVersion.String(), ver)
}
mockUpdate.latestVersion = c.latestVersion
mockUpdate.onUpdate()
ver, _ = waitForUpdateEvent(sub, 500*time.Millisecond)
triggeredLater := ver != ""
if triggeredLater != c.shouldUpdateLater {
t.Errorf("%s: Later update trigger mismatch, expected %v, got %v", c.name, c.shouldUpdateLater, triggeredLater)
}
if triggeredLater && c.latestVersion != nil && ver != c.latestVersion.String() {
t.Errorf("%s: Later update version mismatch, expected %v, got %v", c.name, c.latestVersion.String(), ver)
}
m.Stop()
}
}
func Test_HandleUpdate(t *testing.T) {
testMatrix := []struct {
name string
daemonVersion string
latestVersion *v.Version
expectedVersion string
shouldUpdate bool
}{
{
name: "Install to a specific version should update regardless of if latestVersion is available yet",
daemonVersion: "0.55.0",
latestVersion: nil,
expectedVersion: "0.56.0",
shouldUpdate: true,
},
{
name: "Install to specific version should not update if version matches",
daemonVersion: "0.55.0",
latestVersion: nil,
expectedVersion: "0.55.0",
shouldUpdate: false,
},
{
name: "Install to specific version should not update if current version is newer",
daemonVersion: "0.55.0",
latestVersion: nil,
expectedVersion: "0.54.0",
shouldUpdate: false,
},
{
name: "Install to latest version should update if latest is newer",
daemonVersion: "0.55.0",
latestVersion: v.Must(v.NewSemver("0.56.0")),
expectedVersion: "latest",
shouldUpdate: true,
},
{
name: "Install to latest version should not update if latest == current",
daemonVersion: "0.56.0",
latestVersion: v.Must(v.NewSemver("0.56.0")),
expectedVersion: "latest",
shouldUpdate: false,
},
{
name: "Should not update if daemon version is invalid",
daemonVersion: "development",
latestVersion: v.Must(v.NewSemver("1.0.0")),
expectedVersion: "latest",
shouldUpdate: false,
},
{
name: "Should not update if expecting latest and latest version is unavailable",
daemonVersion: "0.55.0",
latestVersion: nil,
expectedVersion: "latest",
shouldUpdate: false,
},
{
name: "Should not update if expected version is invalid",
daemonVersion: "0.55.0",
latestVersion: nil,
expectedVersion: "development",
shouldUpdate: false,
},
}
for idx, c := range testMatrix {
tmpFile := path.Join(t.TempDir(), fmt.Sprintf("update-test-%d.json", idx))
recorder := peer.NewRecorder("")
sub := recorder.SubscribeToEvents()
defer recorder.UnsubscribeFromEvents(sub)
m := NewManager(recorder, statemanager.New(tmpFile))
m.update = &versionUpdateMock{latestVersion: c.latestVersion}
m.currentVersion = c.daemonVersion
m.autoUpdateSupported = func() bool { return true }
m.Start(context.Background())
m.SetVersion(c.expectedVersion, false)
ver, _ := waitForUpdateEvent(sub, 500*time.Millisecond)
updateTriggered := ver != ""
if updateTriggered {
if c.expectedVersion == "latest" && c.latestVersion != nil && ver != c.latestVersion.String() {
t.Errorf("%s: Version mismatch, expected %v, got %v", c.name, c.latestVersion.String(), ver)
} else if c.expectedVersion != "latest" && c.expectedVersion != "development" && ver != c.expectedVersion {
t.Errorf("%s: Version mismatch, expected %v, got %v", c.name, c.expectedVersion, ver)
}
}
if updateTriggered != c.shouldUpdate {
t.Errorf("%s: Update trigger mismatch, expected %v, got %v", c.name, c.shouldUpdate, updateTriggered)
}
m.Stop()
}
}
func Test_EnforcedMetadata(t *testing.T) {
// Mode 1 (downloadOnly): no enforced metadata
tmpFile := path.Join(t.TempDir(), "update-test-mode1.json")
recorder := peer.NewRecorder("")
sub := recorder.SubscribeToEvents()
defer recorder.UnsubscribeFromEvents(sub)
m := NewManager(recorder, statemanager.New(tmpFile))
m.update = &versionUpdateMock{latestVersion: v.Must(v.NewSemver("1.0.1"))}
m.currentVersion = "1.0.0"
m.Start(context.Background())
m.SetDownloadOnly()
ver, enforced := waitForUpdateEvent(sub, 500*time.Millisecond)
if ver == "" {
t.Fatal("Mode 1: expected new_version_available event")
}
if enforced {
t.Error("Mode 1: expected no enforced metadata")
}
m.Stop()
// Mode 2 (enforced, forceUpdate=false): enforced metadata present, no auto-install
tmpFile2 := path.Join(t.TempDir(), "update-test-mode2.json")
recorder2 := peer.NewRecorder("")
sub2 := recorder2.SubscribeToEvents()
defer recorder2.UnsubscribeFromEvents(sub2)
m2 := NewManager(recorder2, statemanager.New(tmpFile2))
m2.update = &versionUpdateMock{latestVersion: nil}
m2.currentVersion = "1.0.0"
m2.autoUpdateSupported = func() bool { return true }
m2.Start(context.Background())
m2.SetVersion("1.0.1", false)
ver, enforced2 := waitForUpdateEvent(sub2, 500*time.Millisecond)
if ver == "" {
t.Fatal("Mode 2: expected new_version_available event")
}
if !enforced2 {
t.Error("Mode 2: expected enforced metadata")
}
m2.Stop()
}
// ensure the proto import is used
var _ = cProto.SystemEvent_INFO

View File

@@ -0,0 +1,56 @@
package updater
import (
"strconv"
"time"
v "github.com/hashicorp/go-version"
"github.com/netbirdio/netbird/client/internal/peer"
)
type versionUpdateMock struct {
latestVersion *v.Version
onUpdate func()
}
func (m versionUpdateMock) StopWatch() {}
func (m versionUpdateMock) SetDaemonVersion(newVersion string) bool {
return false
}
func (m *versionUpdateMock) SetOnUpdateListener(updateFn func()) {
m.onUpdate = updateFn
}
func (m versionUpdateMock) LatestVersion() *v.Version {
return m.latestVersion
}
func (m versionUpdateMock) StartFetcher() {}
// waitForUpdateEvent waits for a new_version_available event, returns the version string or "" on timeout.
func waitForUpdateEvent(sub *peer.EventSubscription, timeout time.Duration) (version string, enforced bool) {
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case event, ok := <-sub.Events():
if !ok {
return "", false
}
if val, ok := event.Metadata["new_version_available"]; ok {
enforced := false
if raw, ok := event.Metadata["enforced"]; ok {
if parsed, err := strconv.ParseBool(raw); err == nil {
enforced = parsed
}
}
return val, enforced
}
case <-timer.C:
return "", false
}
}
}

View File

@@ -10,7 +10,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/updatemanager/downloader" "github.com/netbirdio/netbird/client/internal/updater/downloader"
) )
const ( const (

View File

@@ -0,0 +1,22 @@
package updater
import (
"context"
"time"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/updater/installer"
)
func isAutoUpdateSupported() bool {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
isBrew := !installer.TypeOfInstaller(ctx).Downloadable()
if isBrew {
log.Warnf("auto-update disabled on Homebrew installation")
return false
}
return true
}

View File

@@ -0,0 +1,7 @@
//go:build !windows && !darwin
package updater
func isAutoUpdateSupported() bool {
return false
}

View File

@@ -0,0 +1,5 @@
package updater
func isAutoUpdateSupported() bool {
return true
}

View File

@@ -1,4 +1,4 @@
package updatemanager package updater
import v "github.com/hashicorp/go-version" import v "github.com/hashicorp/go-version"

View File

@@ -160,7 +160,7 @@ func (c *Client) Run(fd int32, interfaceName string, envList *EnvList) error {
c.onHostDnsFn = func([]string) {} c.onHostDnsFn = func([]string) {}
cfg.WgIface = interfaceName cfg.WgIface = interfaceName
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder, false) c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
return c.connectClient.RunOniOS(fd, c.networkChangeListener, c.dnsManager, c.stateFile) return c.connectClient.RunOniOS(fd, c.networkChangeListener, c.dnsManager, c.stateFile)
} }

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.6 // protoc-gen-go v1.36.6
// protoc v6.33.3 // protoc v6.33.1
// source: daemon.proto // source: daemon.proto
package proto package proto
@@ -945,7 +945,6 @@ type UpRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
ProfileName *string `protobuf:"bytes,1,opt,name=profileName,proto3,oneof" json:"profileName,omitempty"` ProfileName *string `protobuf:"bytes,1,opt,name=profileName,proto3,oneof" json:"profileName,omitempty"`
Username *string `protobuf:"bytes,2,opt,name=username,proto3,oneof" json:"username,omitempty"` Username *string `protobuf:"bytes,2,opt,name=username,proto3,oneof" json:"username,omitempty"`
AutoUpdate *bool `protobuf:"varint,3,opt,name=autoUpdate,proto3,oneof" json:"autoUpdate,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -994,13 +993,6 @@ func (x *UpRequest) GetUsername() string {
return "" return ""
} }
func (x *UpRequest) GetAutoUpdate() bool {
if x != nil && x.AutoUpdate != nil {
return *x.AutoUpdate
}
return false
}
type UpResponse struct { type UpResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@@ -5032,6 +5024,94 @@ func (x *GetFeaturesResponse) GetDisableUpdateSettings() bool {
return false return false
} }
type TriggerUpdateRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *TriggerUpdateRequest) Reset() {
*x = TriggerUpdateRequest{}
mi := &file_daemon_proto_msgTypes[73]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *TriggerUpdateRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TriggerUpdateRequest) ProtoMessage() {}
func (x *TriggerUpdateRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[73]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TriggerUpdateRequest.ProtoReflect.Descriptor instead.
func (*TriggerUpdateRequest) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{73}
}
type TriggerUpdateResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
ErrorMsg string `protobuf:"bytes,2,opt,name=errorMsg,proto3" json:"errorMsg,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *TriggerUpdateResponse) Reset() {
*x = TriggerUpdateResponse{}
mi := &file_daemon_proto_msgTypes[74]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *TriggerUpdateResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TriggerUpdateResponse) ProtoMessage() {}
func (x *TriggerUpdateResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[74]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TriggerUpdateResponse.ProtoReflect.Descriptor instead.
func (*TriggerUpdateResponse) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{74}
}
func (x *TriggerUpdateResponse) GetSuccess() bool {
if x != nil {
return x.Success
}
return false
}
func (x *TriggerUpdateResponse) GetErrorMsg() string {
if x != nil {
return x.ErrorMsg
}
return ""
}
// GetPeerSSHHostKeyRequest for retrieving SSH host key for a specific peer // GetPeerSSHHostKeyRequest for retrieving SSH host key for a specific peer
type GetPeerSSHHostKeyRequest struct { type GetPeerSSHHostKeyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@@ -5043,7 +5123,7 @@ type GetPeerSSHHostKeyRequest struct {
func (x *GetPeerSSHHostKeyRequest) Reset() { func (x *GetPeerSSHHostKeyRequest) Reset() {
*x = GetPeerSSHHostKeyRequest{} *x = GetPeerSSHHostKeyRequest{}
mi := &file_daemon_proto_msgTypes[73] mi := &file_daemon_proto_msgTypes[75]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5055,7 +5135,7 @@ func (x *GetPeerSSHHostKeyRequest) String() string {
func (*GetPeerSSHHostKeyRequest) ProtoMessage() {} func (*GetPeerSSHHostKeyRequest) ProtoMessage() {}
func (x *GetPeerSSHHostKeyRequest) ProtoReflect() protoreflect.Message { func (x *GetPeerSSHHostKeyRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[73] mi := &file_daemon_proto_msgTypes[75]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5068,7 +5148,7 @@ func (x *GetPeerSSHHostKeyRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetPeerSSHHostKeyRequest.ProtoReflect.Descriptor instead. // Deprecated: Use GetPeerSSHHostKeyRequest.ProtoReflect.Descriptor instead.
func (*GetPeerSSHHostKeyRequest) Descriptor() ([]byte, []int) { func (*GetPeerSSHHostKeyRequest) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{73} return file_daemon_proto_rawDescGZIP(), []int{75}
} }
func (x *GetPeerSSHHostKeyRequest) GetPeerAddress() string { func (x *GetPeerSSHHostKeyRequest) GetPeerAddress() string {
@@ -5095,7 +5175,7 @@ type GetPeerSSHHostKeyResponse struct {
func (x *GetPeerSSHHostKeyResponse) Reset() { func (x *GetPeerSSHHostKeyResponse) Reset() {
*x = GetPeerSSHHostKeyResponse{} *x = GetPeerSSHHostKeyResponse{}
mi := &file_daemon_proto_msgTypes[74] mi := &file_daemon_proto_msgTypes[76]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5107,7 +5187,7 @@ func (x *GetPeerSSHHostKeyResponse) String() string {
func (*GetPeerSSHHostKeyResponse) ProtoMessage() {} func (*GetPeerSSHHostKeyResponse) ProtoMessage() {}
func (x *GetPeerSSHHostKeyResponse) ProtoReflect() protoreflect.Message { func (x *GetPeerSSHHostKeyResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[74] mi := &file_daemon_proto_msgTypes[76]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5120,7 +5200,7 @@ func (x *GetPeerSSHHostKeyResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetPeerSSHHostKeyResponse.ProtoReflect.Descriptor instead. // Deprecated: Use GetPeerSSHHostKeyResponse.ProtoReflect.Descriptor instead.
func (*GetPeerSSHHostKeyResponse) Descriptor() ([]byte, []int) { func (*GetPeerSSHHostKeyResponse) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{74} return file_daemon_proto_rawDescGZIP(), []int{76}
} }
func (x *GetPeerSSHHostKeyResponse) GetSshHostKey() []byte { func (x *GetPeerSSHHostKeyResponse) GetSshHostKey() []byte {
@@ -5162,7 +5242,7 @@ type RequestJWTAuthRequest struct {
func (x *RequestJWTAuthRequest) Reset() { func (x *RequestJWTAuthRequest) Reset() {
*x = RequestJWTAuthRequest{} *x = RequestJWTAuthRequest{}
mi := &file_daemon_proto_msgTypes[75] mi := &file_daemon_proto_msgTypes[77]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5174,7 +5254,7 @@ func (x *RequestJWTAuthRequest) String() string {
func (*RequestJWTAuthRequest) ProtoMessage() {} func (*RequestJWTAuthRequest) ProtoMessage() {}
func (x *RequestJWTAuthRequest) ProtoReflect() protoreflect.Message { func (x *RequestJWTAuthRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[75] mi := &file_daemon_proto_msgTypes[77]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5187,7 +5267,7 @@ func (x *RequestJWTAuthRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RequestJWTAuthRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RequestJWTAuthRequest.ProtoReflect.Descriptor instead.
func (*RequestJWTAuthRequest) Descriptor() ([]byte, []int) { func (*RequestJWTAuthRequest) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{75} return file_daemon_proto_rawDescGZIP(), []int{77}
} }
func (x *RequestJWTAuthRequest) GetHint() string { func (x *RequestJWTAuthRequest) GetHint() string {
@@ -5220,7 +5300,7 @@ type RequestJWTAuthResponse struct {
func (x *RequestJWTAuthResponse) Reset() { func (x *RequestJWTAuthResponse) Reset() {
*x = RequestJWTAuthResponse{} *x = RequestJWTAuthResponse{}
mi := &file_daemon_proto_msgTypes[76] mi := &file_daemon_proto_msgTypes[78]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5232,7 +5312,7 @@ func (x *RequestJWTAuthResponse) String() string {
func (*RequestJWTAuthResponse) ProtoMessage() {} func (*RequestJWTAuthResponse) ProtoMessage() {}
func (x *RequestJWTAuthResponse) ProtoReflect() protoreflect.Message { func (x *RequestJWTAuthResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[76] mi := &file_daemon_proto_msgTypes[78]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5245,7 +5325,7 @@ func (x *RequestJWTAuthResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use RequestJWTAuthResponse.ProtoReflect.Descriptor instead. // Deprecated: Use RequestJWTAuthResponse.ProtoReflect.Descriptor instead.
func (*RequestJWTAuthResponse) Descriptor() ([]byte, []int) { func (*RequestJWTAuthResponse) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{76} return file_daemon_proto_rawDescGZIP(), []int{78}
} }
func (x *RequestJWTAuthResponse) GetVerificationURI() string { func (x *RequestJWTAuthResponse) GetVerificationURI() string {
@@ -5310,7 +5390,7 @@ type WaitJWTTokenRequest struct {
func (x *WaitJWTTokenRequest) Reset() { func (x *WaitJWTTokenRequest) Reset() {
*x = WaitJWTTokenRequest{} *x = WaitJWTTokenRequest{}
mi := &file_daemon_proto_msgTypes[77] mi := &file_daemon_proto_msgTypes[79]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5322,7 +5402,7 @@ func (x *WaitJWTTokenRequest) String() string {
func (*WaitJWTTokenRequest) ProtoMessage() {} func (*WaitJWTTokenRequest) ProtoMessage() {}
func (x *WaitJWTTokenRequest) ProtoReflect() protoreflect.Message { func (x *WaitJWTTokenRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[77] mi := &file_daemon_proto_msgTypes[79]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5335,7 +5415,7 @@ func (x *WaitJWTTokenRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use WaitJWTTokenRequest.ProtoReflect.Descriptor instead. // Deprecated: Use WaitJWTTokenRequest.ProtoReflect.Descriptor instead.
func (*WaitJWTTokenRequest) Descriptor() ([]byte, []int) { func (*WaitJWTTokenRequest) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{77} return file_daemon_proto_rawDescGZIP(), []int{79}
} }
func (x *WaitJWTTokenRequest) GetDeviceCode() string { func (x *WaitJWTTokenRequest) GetDeviceCode() string {
@@ -5367,7 +5447,7 @@ type WaitJWTTokenResponse struct {
func (x *WaitJWTTokenResponse) Reset() { func (x *WaitJWTTokenResponse) Reset() {
*x = WaitJWTTokenResponse{} *x = WaitJWTTokenResponse{}
mi := &file_daemon_proto_msgTypes[78] mi := &file_daemon_proto_msgTypes[80]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5379,7 +5459,7 @@ func (x *WaitJWTTokenResponse) String() string {
func (*WaitJWTTokenResponse) ProtoMessage() {} func (*WaitJWTTokenResponse) ProtoMessage() {}
func (x *WaitJWTTokenResponse) ProtoReflect() protoreflect.Message { func (x *WaitJWTTokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[78] mi := &file_daemon_proto_msgTypes[80]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5392,7 +5472,7 @@ func (x *WaitJWTTokenResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use WaitJWTTokenResponse.ProtoReflect.Descriptor instead. // Deprecated: Use WaitJWTTokenResponse.ProtoReflect.Descriptor instead.
func (*WaitJWTTokenResponse) Descriptor() ([]byte, []int) { func (*WaitJWTTokenResponse) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{78} return file_daemon_proto_rawDescGZIP(), []int{80}
} }
func (x *WaitJWTTokenResponse) GetToken() string { func (x *WaitJWTTokenResponse) GetToken() string {
@@ -5425,7 +5505,7 @@ type StartCPUProfileRequest struct {
func (x *StartCPUProfileRequest) Reset() { func (x *StartCPUProfileRequest) Reset() {
*x = StartCPUProfileRequest{} *x = StartCPUProfileRequest{}
mi := &file_daemon_proto_msgTypes[79] mi := &file_daemon_proto_msgTypes[81]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5437,7 +5517,7 @@ func (x *StartCPUProfileRequest) String() string {
func (*StartCPUProfileRequest) ProtoMessage() {} func (*StartCPUProfileRequest) ProtoMessage() {}
func (x *StartCPUProfileRequest) ProtoReflect() protoreflect.Message { func (x *StartCPUProfileRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[79] mi := &file_daemon_proto_msgTypes[81]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5450,7 +5530,7 @@ func (x *StartCPUProfileRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use StartCPUProfileRequest.ProtoReflect.Descriptor instead. // Deprecated: Use StartCPUProfileRequest.ProtoReflect.Descriptor instead.
func (*StartCPUProfileRequest) Descriptor() ([]byte, []int) { func (*StartCPUProfileRequest) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{79} return file_daemon_proto_rawDescGZIP(), []int{81}
} }
// StartCPUProfileResponse confirms CPU profiling has started // StartCPUProfileResponse confirms CPU profiling has started
@@ -5462,7 +5542,7 @@ type StartCPUProfileResponse struct {
func (x *StartCPUProfileResponse) Reset() { func (x *StartCPUProfileResponse) Reset() {
*x = StartCPUProfileResponse{} *x = StartCPUProfileResponse{}
mi := &file_daemon_proto_msgTypes[80] mi := &file_daemon_proto_msgTypes[82]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5474,7 +5554,7 @@ func (x *StartCPUProfileResponse) String() string {
func (*StartCPUProfileResponse) ProtoMessage() {} func (*StartCPUProfileResponse) ProtoMessage() {}
func (x *StartCPUProfileResponse) ProtoReflect() protoreflect.Message { func (x *StartCPUProfileResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[80] mi := &file_daemon_proto_msgTypes[82]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5487,7 +5567,7 @@ func (x *StartCPUProfileResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use StartCPUProfileResponse.ProtoReflect.Descriptor instead. // Deprecated: Use StartCPUProfileResponse.ProtoReflect.Descriptor instead.
func (*StartCPUProfileResponse) Descriptor() ([]byte, []int) { func (*StartCPUProfileResponse) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{80} return file_daemon_proto_rawDescGZIP(), []int{82}
} }
// StopCPUProfileRequest for stopping CPU profiling // StopCPUProfileRequest for stopping CPU profiling
@@ -5499,7 +5579,7 @@ type StopCPUProfileRequest struct {
func (x *StopCPUProfileRequest) Reset() { func (x *StopCPUProfileRequest) Reset() {
*x = StopCPUProfileRequest{} *x = StopCPUProfileRequest{}
mi := &file_daemon_proto_msgTypes[81] mi := &file_daemon_proto_msgTypes[83]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5511,7 +5591,7 @@ func (x *StopCPUProfileRequest) String() string {
func (*StopCPUProfileRequest) ProtoMessage() {} func (*StopCPUProfileRequest) ProtoMessage() {}
func (x *StopCPUProfileRequest) ProtoReflect() protoreflect.Message { func (x *StopCPUProfileRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[81] mi := &file_daemon_proto_msgTypes[83]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5524,7 +5604,7 @@ func (x *StopCPUProfileRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use StopCPUProfileRequest.ProtoReflect.Descriptor instead. // Deprecated: Use StopCPUProfileRequest.ProtoReflect.Descriptor instead.
func (*StopCPUProfileRequest) Descriptor() ([]byte, []int) { func (*StopCPUProfileRequest) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{81} return file_daemon_proto_rawDescGZIP(), []int{83}
} }
// StopCPUProfileResponse confirms CPU profiling has stopped // StopCPUProfileResponse confirms CPU profiling has stopped
@@ -5536,7 +5616,7 @@ type StopCPUProfileResponse struct {
func (x *StopCPUProfileResponse) Reset() { func (x *StopCPUProfileResponse) Reset() {
*x = StopCPUProfileResponse{} *x = StopCPUProfileResponse{}
mi := &file_daemon_proto_msgTypes[82] mi := &file_daemon_proto_msgTypes[84]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5548,7 +5628,7 @@ func (x *StopCPUProfileResponse) String() string {
func (*StopCPUProfileResponse) ProtoMessage() {} func (*StopCPUProfileResponse) ProtoMessage() {}
func (x *StopCPUProfileResponse) ProtoReflect() protoreflect.Message { func (x *StopCPUProfileResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[82] mi := &file_daemon_proto_msgTypes[84]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5561,7 +5641,7 @@ func (x *StopCPUProfileResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use StopCPUProfileResponse.ProtoReflect.Descriptor instead. // Deprecated: Use StopCPUProfileResponse.ProtoReflect.Descriptor instead.
func (*StopCPUProfileResponse) Descriptor() ([]byte, []int) { func (*StopCPUProfileResponse) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{82} return file_daemon_proto_rawDescGZIP(), []int{84}
} }
type InstallerResultRequest struct { type InstallerResultRequest struct {
@@ -5572,7 +5652,7 @@ type InstallerResultRequest struct {
func (x *InstallerResultRequest) Reset() { func (x *InstallerResultRequest) Reset() {
*x = InstallerResultRequest{} *x = InstallerResultRequest{}
mi := &file_daemon_proto_msgTypes[83] mi := &file_daemon_proto_msgTypes[85]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5584,7 +5664,7 @@ func (x *InstallerResultRequest) String() string {
func (*InstallerResultRequest) ProtoMessage() {} func (*InstallerResultRequest) ProtoMessage() {}
func (x *InstallerResultRequest) ProtoReflect() protoreflect.Message { func (x *InstallerResultRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[83] mi := &file_daemon_proto_msgTypes[85]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5597,7 +5677,7 @@ func (x *InstallerResultRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InstallerResultRequest.ProtoReflect.Descriptor instead. // Deprecated: Use InstallerResultRequest.ProtoReflect.Descriptor instead.
func (*InstallerResultRequest) Descriptor() ([]byte, []int) { func (*InstallerResultRequest) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{83} return file_daemon_proto_rawDescGZIP(), []int{85}
} }
type InstallerResultResponse struct { type InstallerResultResponse struct {
@@ -5610,7 +5690,7 @@ type InstallerResultResponse struct {
func (x *InstallerResultResponse) Reset() { func (x *InstallerResultResponse) Reset() {
*x = InstallerResultResponse{} *x = InstallerResultResponse{}
mi := &file_daemon_proto_msgTypes[84] mi := &file_daemon_proto_msgTypes[86]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5622,7 +5702,7 @@ func (x *InstallerResultResponse) String() string {
func (*InstallerResultResponse) ProtoMessage() {} func (*InstallerResultResponse) ProtoMessage() {}
func (x *InstallerResultResponse) ProtoReflect() protoreflect.Message { func (x *InstallerResultResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[84] mi := &file_daemon_proto_msgTypes[86]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5635,7 +5715,7 @@ func (x *InstallerResultResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InstallerResultResponse.ProtoReflect.Descriptor instead. // Deprecated: Use InstallerResultResponse.ProtoReflect.Descriptor instead.
func (*InstallerResultResponse) Descriptor() ([]byte, []int) { func (*InstallerResultResponse) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{84} return file_daemon_proto_rawDescGZIP(), []int{86}
} }
func (x *InstallerResultResponse) GetSuccess() bool { func (x *InstallerResultResponse) GetSuccess() bool {
@@ -5667,7 +5747,7 @@ type ExposeServiceRequest struct {
func (x *ExposeServiceRequest) Reset() { func (x *ExposeServiceRequest) Reset() {
*x = ExposeServiceRequest{} *x = ExposeServiceRequest{}
mi := &file_daemon_proto_msgTypes[85] mi := &file_daemon_proto_msgTypes[87]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5679,7 +5759,7 @@ func (x *ExposeServiceRequest) String() string {
func (*ExposeServiceRequest) ProtoMessage() {} func (*ExposeServiceRequest) ProtoMessage() {}
func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[85] mi := &file_daemon_proto_msgTypes[87]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5692,7 +5772,7 @@ func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead.
func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { func (*ExposeServiceRequest) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{85} return file_daemon_proto_rawDescGZIP(), []int{87}
} }
func (x *ExposeServiceRequest) GetPort() uint32 { func (x *ExposeServiceRequest) GetPort() uint32 {
@@ -5756,7 +5836,7 @@ type ExposeServiceEvent struct {
func (x *ExposeServiceEvent) Reset() { func (x *ExposeServiceEvent) Reset() {
*x = ExposeServiceEvent{} *x = ExposeServiceEvent{}
mi := &file_daemon_proto_msgTypes[86] mi := &file_daemon_proto_msgTypes[88]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5768,7 +5848,7 @@ func (x *ExposeServiceEvent) String() string {
func (*ExposeServiceEvent) ProtoMessage() {} func (*ExposeServiceEvent) ProtoMessage() {}
func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message { func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[86] mi := &file_daemon_proto_msgTypes[88]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5781,7 +5861,7 @@ func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExposeServiceEvent.ProtoReflect.Descriptor instead. // Deprecated: Use ExposeServiceEvent.ProtoReflect.Descriptor instead.
func (*ExposeServiceEvent) Descriptor() ([]byte, []int) { func (*ExposeServiceEvent) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{86} return file_daemon_proto_rawDescGZIP(), []int{88}
} }
func (x *ExposeServiceEvent) GetEvent() isExposeServiceEvent_Event { func (x *ExposeServiceEvent) GetEvent() isExposeServiceEvent_Event {
@@ -5821,7 +5901,7 @@ type ExposeServiceReady struct {
func (x *ExposeServiceReady) Reset() { func (x *ExposeServiceReady) Reset() {
*x = ExposeServiceReady{} *x = ExposeServiceReady{}
mi := &file_daemon_proto_msgTypes[87] mi := &file_daemon_proto_msgTypes[89]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5833,7 +5913,7 @@ func (x *ExposeServiceReady) String() string {
func (*ExposeServiceReady) ProtoMessage() {} func (*ExposeServiceReady) ProtoMessage() {}
func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message { func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[87] mi := &file_daemon_proto_msgTypes[89]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5846,7 +5926,7 @@ func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExposeServiceReady.ProtoReflect.Descriptor instead. // Deprecated: Use ExposeServiceReady.ProtoReflect.Descriptor instead.
func (*ExposeServiceReady) Descriptor() ([]byte, []int) { func (*ExposeServiceReady) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{87} return file_daemon_proto_rawDescGZIP(), []int{89}
} }
func (x *ExposeServiceReady) GetServiceName() string { func (x *ExposeServiceReady) GetServiceName() string {
@@ -5880,7 +5960,7 @@ type PortInfo_Range struct {
func (x *PortInfo_Range) Reset() { func (x *PortInfo_Range) Reset() {
*x = PortInfo_Range{} *x = PortInfo_Range{}
mi := &file_daemon_proto_msgTypes[89] mi := &file_daemon_proto_msgTypes[91]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -5892,7 +5972,7 @@ func (x *PortInfo_Range) String() string {
func (*PortInfo_Range) ProtoMessage() {} func (*PortInfo_Range) ProtoMessage() {}
func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { func (x *PortInfo_Range) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[89] mi := &file_daemon_proto_msgTypes[91]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -6016,16 +6096,12 @@ const file_daemon_proto_rawDesc = "" +
"\buserCode\x18\x01 \x01(\tR\buserCode\x12\x1a\n" + "\buserCode\x18\x01 \x01(\tR\buserCode\x12\x1a\n" +
"\bhostname\x18\x02 \x01(\tR\bhostname\",\n" + "\bhostname\x18\x02 \x01(\tR\bhostname\",\n" +
"\x14WaitSSOLoginResponse\x12\x14\n" + "\x14WaitSSOLoginResponse\x12\x14\n" +
"\x05email\x18\x01 \x01(\tR\x05email\"\xa4\x01\n" + "\x05email\x18\x01 \x01(\tR\x05email\"v\n" +
"\tUpRequest\x12%\n" + "\tUpRequest\x12%\n" +
"\vprofileName\x18\x01 \x01(\tH\x00R\vprofileName\x88\x01\x01\x12\x1f\n" + "\vprofileName\x18\x01 \x01(\tH\x00R\vprofileName\x88\x01\x01\x12\x1f\n" +
"\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01\x12#\n" + "\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01B\x0e\n" +
"\n" +
"autoUpdate\x18\x03 \x01(\bH\x02R\n" +
"autoUpdate\x88\x01\x01B\x0e\n" +
"\f_profileNameB\v\n" + "\f_profileNameB\v\n" +
"\t_usernameB\r\n" + "\t_usernameJ\x04\b\x03\x10\x04\"\f\n" +
"\v_autoUpdate\"\f\n" +
"\n" + "\n" +
"UpResponse\"\xa1\x01\n" + "UpResponse\"\xa1\x01\n" +
"\rStatusRequest\x12,\n" + "\rStatusRequest\x12,\n" +
@@ -6380,7 +6456,11 @@ const file_daemon_proto_rawDesc = "" +
"\x12GetFeaturesRequest\"x\n" + "\x12GetFeaturesRequest\"x\n" +
"\x13GetFeaturesResponse\x12)\n" + "\x13GetFeaturesResponse\x12)\n" +
"\x10disable_profiles\x18\x01 \x01(\bR\x0fdisableProfiles\x126\n" + "\x10disable_profiles\x18\x01 \x01(\bR\x0fdisableProfiles\x126\n" +
"\x17disable_update_settings\x18\x02 \x01(\bR\x15disableUpdateSettings\"<\n" + "\x17disable_update_settings\x18\x02 \x01(\bR\x15disableUpdateSettings\"\x16\n" +
"\x14TriggerUpdateRequest\"M\n" +
"\x15TriggerUpdateResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1a\n" +
"\berrorMsg\x18\x02 \x01(\tR\berrorMsg\"<\n" +
"\x18GetPeerSSHHostKeyRequest\x12 \n" + "\x18GetPeerSSHHostKeyRequest\x12 \n" +
"\vpeerAddress\x18\x01 \x01(\tR\vpeerAddress\"\x85\x01\n" + "\vpeerAddress\x18\x01 \x01(\tR\vpeerAddress\"\x85\x01\n" +
"\x19GetPeerSSHHostKeyResponse\x12\x1e\n" + "\x19GetPeerSSHHostKeyResponse\x12\x1e\n" +
@@ -6453,7 +6533,7 @@ const file_daemon_proto_rawDesc = "" +
"\n" + "\n" +
"EXPOSE_TCP\x10\x02\x12\x0e\n" + "EXPOSE_TCP\x10\x02\x12\x0e\n" +
"\n" + "\n" +
"EXPOSE_UDP\x10\x032\xac\x15\n" + "EXPOSE_UDP\x10\x032\xfc\x15\n" +
"\rDaemonService\x126\n" + "\rDaemonService\x126\n" +
"\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" + "\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" +
"\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" + "\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" +
@@ -6485,7 +6565,8 @@ const file_daemon_proto_rawDesc = "" +
"\fListProfiles\x12\x1b.daemon.ListProfilesRequest\x1a\x1c.daemon.ListProfilesResponse\"\x00\x12W\n" + "\fListProfiles\x12\x1b.daemon.ListProfilesRequest\x1a\x1c.daemon.ListProfilesResponse\"\x00\x12W\n" +
"\x10GetActiveProfile\x12\x1f.daemon.GetActiveProfileRequest\x1a .daemon.GetActiveProfileResponse\"\x00\x129\n" + "\x10GetActiveProfile\x12\x1f.daemon.GetActiveProfileRequest\x1a .daemon.GetActiveProfileResponse\"\x00\x129\n" +
"\x06Logout\x12\x15.daemon.LogoutRequest\x1a\x16.daemon.LogoutResponse\"\x00\x12H\n" + "\x06Logout\x12\x15.daemon.LogoutRequest\x1a\x16.daemon.LogoutResponse\"\x00\x12H\n" +
"\vGetFeatures\x12\x1a.daemon.GetFeaturesRequest\x1a\x1b.daemon.GetFeaturesResponse\"\x00\x12Z\n" + "\vGetFeatures\x12\x1a.daemon.GetFeaturesRequest\x1a\x1b.daemon.GetFeaturesResponse\"\x00\x12N\n" +
"\rTriggerUpdate\x12\x1c.daemon.TriggerUpdateRequest\x1a\x1d.daemon.TriggerUpdateResponse\"\x00\x12Z\n" +
"\x11GetPeerSSHHostKey\x12 .daemon.GetPeerSSHHostKeyRequest\x1a!.daemon.GetPeerSSHHostKeyResponse\"\x00\x12Q\n" + "\x11GetPeerSSHHostKey\x12 .daemon.GetPeerSSHHostKeyRequest\x1a!.daemon.GetPeerSSHHostKeyResponse\"\x00\x12Q\n" +
"\x0eRequestJWTAuth\x12\x1d.daemon.RequestJWTAuthRequest\x1a\x1e.daemon.RequestJWTAuthResponse\"\x00\x12K\n" + "\x0eRequestJWTAuth\x12\x1d.daemon.RequestJWTAuthRequest\x1a\x1e.daemon.RequestJWTAuthResponse\"\x00\x12K\n" +
"\fWaitJWTToken\x12\x1b.daemon.WaitJWTTokenRequest\x1a\x1c.daemon.WaitJWTTokenResponse\"\x00\x12T\n" + "\fWaitJWTToken\x12\x1b.daemon.WaitJWTTokenRequest\x1a\x1c.daemon.WaitJWTTokenResponse\"\x00\x12T\n" +
@@ -6508,7 +6589,7 @@ func file_daemon_proto_rawDescGZIP() []byte {
} }
var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 5) var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 91) var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 93)
var file_daemon_proto_goTypes = []any{ var file_daemon_proto_goTypes = []any{
(LogLevel)(0), // 0: daemon.LogLevel (LogLevel)(0), // 0: daemon.LogLevel
(ExposeProtocol)(0), // 1: daemon.ExposeProtocol (ExposeProtocol)(0), // 1: daemon.ExposeProtocol
@@ -6588,34 +6669,36 @@ var file_daemon_proto_goTypes = []any{
(*LogoutResponse)(nil), // 75: daemon.LogoutResponse (*LogoutResponse)(nil), // 75: daemon.LogoutResponse
(*GetFeaturesRequest)(nil), // 76: daemon.GetFeaturesRequest (*GetFeaturesRequest)(nil), // 76: daemon.GetFeaturesRequest
(*GetFeaturesResponse)(nil), // 77: daemon.GetFeaturesResponse (*GetFeaturesResponse)(nil), // 77: daemon.GetFeaturesResponse
(*GetPeerSSHHostKeyRequest)(nil), // 78: daemon.GetPeerSSHHostKeyRequest (*TriggerUpdateRequest)(nil), // 78: daemon.TriggerUpdateRequest
(*GetPeerSSHHostKeyResponse)(nil), // 79: daemon.GetPeerSSHHostKeyResponse (*TriggerUpdateResponse)(nil), // 79: daemon.TriggerUpdateResponse
(*RequestJWTAuthRequest)(nil), // 80: daemon.RequestJWTAuthRequest (*GetPeerSSHHostKeyRequest)(nil), // 80: daemon.GetPeerSSHHostKeyRequest
(*RequestJWTAuthResponse)(nil), // 81: daemon.RequestJWTAuthResponse (*GetPeerSSHHostKeyResponse)(nil), // 81: daemon.GetPeerSSHHostKeyResponse
(*WaitJWTTokenRequest)(nil), // 82: daemon.WaitJWTTokenRequest (*RequestJWTAuthRequest)(nil), // 82: daemon.RequestJWTAuthRequest
(*WaitJWTTokenResponse)(nil), // 83: daemon.WaitJWTTokenResponse (*RequestJWTAuthResponse)(nil), // 83: daemon.RequestJWTAuthResponse
(*StartCPUProfileRequest)(nil), // 84: daemon.StartCPUProfileRequest (*WaitJWTTokenRequest)(nil), // 84: daemon.WaitJWTTokenRequest
(*StartCPUProfileResponse)(nil), // 85: daemon.StartCPUProfileResponse (*WaitJWTTokenResponse)(nil), // 85: daemon.WaitJWTTokenResponse
(*StopCPUProfileRequest)(nil), // 86: daemon.StopCPUProfileRequest (*StartCPUProfileRequest)(nil), // 86: daemon.StartCPUProfileRequest
(*StopCPUProfileResponse)(nil), // 87: daemon.StopCPUProfileResponse (*StartCPUProfileResponse)(nil), // 87: daemon.StartCPUProfileResponse
(*InstallerResultRequest)(nil), // 88: daemon.InstallerResultRequest (*StopCPUProfileRequest)(nil), // 88: daemon.StopCPUProfileRequest
(*InstallerResultResponse)(nil), // 89: daemon.InstallerResultResponse (*StopCPUProfileResponse)(nil), // 89: daemon.StopCPUProfileResponse
(*ExposeServiceRequest)(nil), // 90: daemon.ExposeServiceRequest (*InstallerResultRequest)(nil), // 90: daemon.InstallerResultRequest
(*ExposeServiceEvent)(nil), // 91: daemon.ExposeServiceEvent (*InstallerResultResponse)(nil), // 91: daemon.InstallerResultResponse
(*ExposeServiceReady)(nil), // 92: daemon.ExposeServiceReady (*ExposeServiceRequest)(nil), // 92: daemon.ExposeServiceRequest
nil, // 93: daemon.Network.ResolvedIPsEntry (*ExposeServiceEvent)(nil), // 93: daemon.ExposeServiceEvent
(*PortInfo_Range)(nil), // 94: daemon.PortInfo.Range (*ExposeServiceReady)(nil), // 94: daemon.ExposeServiceReady
nil, // 95: daemon.SystemEvent.MetadataEntry nil, // 95: daemon.Network.ResolvedIPsEntry
(*durationpb.Duration)(nil), // 96: google.protobuf.Duration (*PortInfo_Range)(nil), // 96: daemon.PortInfo.Range
(*timestamppb.Timestamp)(nil), // 97: google.protobuf.Timestamp nil, // 97: daemon.SystemEvent.MetadataEntry
(*durationpb.Duration)(nil), // 98: google.protobuf.Duration
(*timestamppb.Timestamp)(nil), // 99: google.protobuf.Timestamp
} }
var file_daemon_proto_depIdxs = []int32{ var file_daemon_proto_depIdxs = []int32{
2, // 0: daemon.OSLifecycleRequest.type:type_name -> daemon.OSLifecycleRequest.CycleType 2, // 0: daemon.OSLifecycleRequest.type:type_name -> daemon.OSLifecycleRequest.CycleType
96, // 1: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 98, // 1: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
28, // 2: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus 28, // 2: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
97, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp 99, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
97, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp 99, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
96, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration 98, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration
26, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo 26, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo
23, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 23, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
22, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState 22, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState
@@ -6626,8 +6709,8 @@ var file_daemon_proto_depIdxs = []int32{
58, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent 58, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent
27, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState 27, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState
34, // 15: daemon.ListNetworksResponse.routes:type_name -> daemon.Network 34, // 15: daemon.ListNetworksResponse.routes:type_name -> daemon.Network
93, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry 95, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry
94, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range 96, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range
35, // 18: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo 35, // 18: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo
35, // 19: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo 35, // 19: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo
36, // 20: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule 36, // 20: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule
@@ -6638,13 +6721,13 @@ var file_daemon_proto_depIdxs = []int32{
55, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage 55, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage
3, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity 3, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity
4, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category 4, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category
97, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp 99, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp
95, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry 97, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry
58, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent 58, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent
96, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 98, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
71, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile 71, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile
1, // 33: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol 1, // 33: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol
92, // 34: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady 94, // 34: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady
33, // 35: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList 33, // 35: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList
8, // 36: daemon.DaemonService.Login:input_type -> daemon.LoginRequest 8, // 36: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
10, // 37: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest 10, // 37: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
@@ -6674,52 +6757,54 @@ var file_daemon_proto_depIdxs = []int32{
72, // 61: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest 72, // 61: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest
74, // 62: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest 74, // 62: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest
76, // 63: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest 76, // 63: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest
78, // 64: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest 78, // 64: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest
80, // 65: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest 80, // 65: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest
82, // 66: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest 82, // 66: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest
84, // 67: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest 84, // 67: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest
86, // 68: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest 86, // 68: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest
6, // 69: daemon.DaemonService.NotifyOSLifecycle:input_type -> daemon.OSLifecycleRequest 88, // 69: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest
88, // 70: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest 6, // 70: daemon.DaemonService.NotifyOSLifecycle:input_type -> daemon.OSLifecycleRequest
90, // 71: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest 90, // 71: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest
9, // 72: daemon.DaemonService.Login:output_type -> daemon.LoginResponse 92, // 72: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest
11, // 73: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse 9, // 73: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
13, // 74: daemon.DaemonService.Up:output_type -> daemon.UpResponse 11, // 74: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
15, // 75: daemon.DaemonService.Status:output_type -> daemon.StatusResponse 13, // 75: daemon.DaemonService.Up:output_type -> daemon.UpResponse
17, // 76: daemon.DaemonService.Down:output_type -> daemon.DownResponse 15, // 76: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
19, // 77: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse 17, // 77: daemon.DaemonService.Down:output_type -> daemon.DownResponse
30, // 78: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse 19, // 78: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
32, // 79: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse 30, // 79: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse
32, // 80: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse 32, // 80: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse
37, // 81: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse 32, // 81: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse
39, // 82: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse 37, // 82: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse
41, // 83: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse 39, // 83: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse
43, // 84: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse 41, // 84: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse
46, // 85: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse 43, // 85: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse
48, // 86: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse 46, // 86: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse
50, // 87: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse 48, // 87: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse
52, // 88: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse 50, // 88: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse
56, // 89: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse 52, // 89: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse
58, // 90: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent 56, // 90: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse
60, // 91: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse 58, // 91: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent
62, // 92: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse 60, // 92: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse
64, // 93: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse 62, // 93: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse
66, // 94: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse 64, // 94: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse
68, // 95: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse 66, // 95: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse
70, // 96: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse 68, // 96: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse
73, // 97: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse 70, // 97: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse
75, // 98: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse 73, // 98: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse
77, // 99: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse 75, // 99: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse
79, // 100: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse 77, // 100: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse
81, // 101: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse 79, // 101: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse
83, // 102: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse 81, // 102: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse
85, // 103: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse 83, // 103: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse
87, // 104: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse 85, // 104: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse
7, // 105: daemon.DaemonService.NotifyOSLifecycle:output_type -> daemon.OSLifecycleResponse 87, // 105: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse
89, // 106: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse 89, // 106: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse
91, // 107: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent 7, // 107: daemon.DaemonService.NotifyOSLifecycle:output_type -> daemon.OSLifecycleResponse
72, // [72:108] is the sub-list for method output_type 91, // 108: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse
36, // [36:72] is the sub-list for method input_type 93, // 109: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent
73, // [73:110] is the sub-list for method output_type
36, // [36:73] is the sub-list for method input_type
36, // [36:36] is the sub-list for extension type_name 36, // [36:36] is the sub-list for extension type_name
36, // [36:36] is the sub-list for extension extendee 36, // [36:36] is the sub-list for extension extendee
0, // [0:36] is the sub-list for field type_name 0, // [0:36] is the sub-list for field type_name
@@ -6742,8 +6827,8 @@ func file_daemon_proto_init() {
file_daemon_proto_msgTypes[56].OneofWrappers = []any{} file_daemon_proto_msgTypes[56].OneofWrappers = []any{}
file_daemon_proto_msgTypes[58].OneofWrappers = []any{} file_daemon_proto_msgTypes[58].OneofWrappers = []any{}
file_daemon_proto_msgTypes[69].OneofWrappers = []any{} file_daemon_proto_msgTypes[69].OneofWrappers = []any{}
file_daemon_proto_msgTypes[75].OneofWrappers = []any{} file_daemon_proto_msgTypes[77].OneofWrappers = []any{}
file_daemon_proto_msgTypes[86].OneofWrappers = []any{ file_daemon_proto_msgTypes[88].OneofWrappers = []any{
(*ExposeServiceEvent_Ready)(nil), (*ExposeServiceEvent_Ready)(nil),
} }
type x struct{} type x struct{}
@@ -6752,7 +6837,7 @@ func file_daemon_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)),
NumEnums: 5, NumEnums: 5,
NumMessages: 91, NumMessages: 93,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@@ -85,6 +85,10 @@ service DaemonService {
rpc GetFeatures(GetFeaturesRequest) returns (GetFeaturesResponse) {} rpc GetFeatures(GetFeaturesRequest) returns (GetFeaturesResponse) {}
// TriggerUpdate initiates installation of the pending enforced version.
// Called when the user clicks the install button in the UI (Mode 2 / enforced update).
rpc TriggerUpdate(TriggerUpdateRequest) returns (TriggerUpdateResponse) {}
// GetPeerSSHHostKey retrieves SSH host key for a specific peer // GetPeerSSHHostKey retrieves SSH host key for a specific peer
rpc GetPeerSSHHostKey(GetPeerSSHHostKeyRequest) returns (GetPeerSSHHostKeyResponse) {} rpc GetPeerSSHHostKey(GetPeerSSHHostKeyRequest) returns (GetPeerSSHHostKeyResponse) {}
@@ -226,7 +230,7 @@ message WaitSSOLoginResponse {
message UpRequest { message UpRequest {
optional string profileName = 1; optional string profileName = 1;
optional string username = 2; optional string username = 2;
optional bool autoUpdate = 3; reserved 3;
} }
message UpResponse {} message UpResponse {}
@@ -725,6 +729,13 @@ message GetFeaturesResponse{
bool disable_update_settings = 2; bool disable_update_settings = 2;
} }
message TriggerUpdateRequest {}
message TriggerUpdateResponse {
bool success = 1;
string errorMsg = 2;
}
// GetPeerSSHHostKeyRequest for retrieving SSH host key for a specific peer // GetPeerSSHHostKeyRequest for retrieving SSH host key for a specific peer
message GetPeerSSHHostKeyRequest { message GetPeerSSHHostKeyRequest {
// peer IP address or FQDN to get SSH host key for // peer IP address or FQDN to get SSH host key for

View File

@@ -64,6 +64,9 @@ type DaemonServiceClient interface {
// Logout disconnects from the network and deletes the peer from the management server // Logout disconnects from the network and deletes the peer from the management server
Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error)
GetFeatures(ctx context.Context, in *GetFeaturesRequest, opts ...grpc.CallOption) (*GetFeaturesResponse, error) GetFeatures(ctx context.Context, in *GetFeaturesRequest, opts ...grpc.CallOption) (*GetFeaturesResponse, error)
// TriggerUpdate initiates installation of the pending enforced version.
// Called when the user clicks the install button in the UI (Mode 2 / enforced update).
TriggerUpdate(ctx context.Context, in *TriggerUpdateRequest, opts ...grpc.CallOption) (*TriggerUpdateResponse, error)
// GetPeerSSHHostKey retrieves SSH host key for a specific peer // GetPeerSSHHostKey retrieves SSH host key for a specific peer
GetPeerSSHHostKey(ctx context.Context, in *GetPeerSSHHostKeyRequest, opts ...grpc.CallOption) (*GetPeerSSHHostKeyResponse, error) GetPeerSSHHostKey(ctx context.Context, in *GetPeerSSHHostKeyRequest, opts ...grpc.CallOption) (*GetPeerSSHHostKeyResponse, error)
// RequestJWTAuth initiates JWT authentication flow for SSH // RequestJWTAuth initiates JWT authentication flow for SSH
@@ -363,6 +366,15 @@ func (c *daemonServiceClient) GetFeatures(ctx context.Context, in *GetFeaturesRe
return out, nil return out, nil
} }
func (c *daemonServiceClient) TriggerUpdate(ctx context.Context, in *TriggerUpdateRequest, opts ...grpc.CallOption) (*TriggerUpdateResponse, error) {
out := new(TriggerUpdateResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/TriggerUpdate", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *daemonServiceClient) GetPeerSSHHostKey(ctx context.Context, in *GetPeerSSHHostKeyRequest, opts ...grpc.CallOption) (*GetPeerSSHHostKeyResponse, error) { func (c *daemonServiceClient) GetPeerSSHHostKey(ctx context.Context, in *GetPeerSSHHostKeyRequest, opts ...grpc.CallOption) (*GetPeerSSHHostKeyResponse, error) {
out := new(GetPeerSSHHostKeyResponse) out := new(GetPeerSSHHostKeyResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetPeerSSHHostKey", in, out, opts...) err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetPeerSSHHostKey", in, out, opts...)
@@ -508,6 +520,9 @@ type DaemonServiceServer interface {
// Logout disconnects from the network and deletes the peer from the management server // Logout disconnects from the network and deletes the peer from the management server
Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error)
GetFeatures(context.Context, *GetFeaturesRequest) (*GetFeaturesResponse, error) GetFeatures(context.Context, *GetFeaturesRequest) (*GetFeaturesResponse, error)
// TriggerUpdate initiates installation of the pending enforced version.
// Called when the user clicks the install button in the UI (Mode 2 / enforced update).
TriggerUpdate(context.Context, *TriggerUpdateRequest) (*TriggerUpdateResponse, error)
// GetPeerSSHHostKey retrieves SSH host key for a specific peer // GetPeerSSHHostKey retrieves SSH host key for a specific peer
GetPeerSSHHostKey(context.Context, *GetPeerSSHHostKeyRequest) (*GetPeerSSHHostKeyResponse, error) GetPeerSSHHostKey(context.Context, *GetPeerSSHHostKeyRequest) (*GetPeerSSHHostKeyResponse, error)
// RequestJWTAuth initiates JWT authentication flow for SSH // RequestJWTAuth initiates JWT authentication flow for SSH
@@ -613,6 +628,9 @@ func (UnimplementedDaemonServiceServer) Logout(context.Context, *LogoutRequest)
func (UnimplementedDaemonServiceServer) GetFeatures(context.Context, *GetFeaturesRequest) (*GetFeaturesResponse, error) { func (UnimplementedDaemonServiceServer) GetFeatures(context.Context, *GetFeaturesRequest) (*GetFeaturesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetFeatures not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetFeatures not implemented")
} }
func (UnimplementedDaemonServiceServer) TriggerUpdate(context.Context, *TriggerUpdateRequest) (*TriggerUpdateResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method TriggerUpdate not implemented")
}
func (UnimplementedDaemonServiceServer) GetPeerSSHHostKey(context.Context, *GetPeerSSHHostKeyRequest) (*GetPeerSSHHostKeyResponse, error) { func (UnimplementedDaemonServiceServer) GetPeerSSHHostKey(context.Context, *GetPeerSSHHostKeyRequest) (*GetPeerSSHHostKeyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetPeerSSHHostKey not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetPeerSSHHostKey not implemented")
} }
@@ -1157,6 +1175,24 @@ func _DaemonService_GetFeatures_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _DaemonService_TriggerUpdate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TriggerUpdateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).TriggerUpdate(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/TriggerUpdate",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).TriggerUpdate(ctx, req.(*TriggerUpdateRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DaemonService_GetPeerSSHHostKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _DaemonService_GetPeerSSHHostKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetPeerSSHHostKeyRequest) in := new(GetPeerSSHHostKeyRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -1419,6 +1455,10 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetFeatures", MethodName: "GetFeatures",
Handler: _DaemonService_GetFeatures_Handler, Handler: _DaemonService_GetFeatures_Handler,
}, },
{
MethodName: "TriggerUpdate",
Handler: _DaemonService_TriggerUpdate_Handler,
},
{ {
MethodName: "GetPeerSSHHostKey", MethodName: "GetPeerSSHHostKey",
Handler: _DaemonService_GetPeerSSHHostKey_Handler, Handler: _DaemonService_GetPeerSSHHostKey_Handler,

View File

@@ -14,6 +14,7 @@ func (s *Server) SubscribeEvents(req *proto.SubscribeRequest, stream proto.Daemo
}() }()
log.Debug("client subscribed to events") log.Debug("client subscribed to events")
s.startUpdateManagerForGUI()
for { for {
select { select {

View File

@@ -30,6 +30,8 @@ import (
"github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/statemanager"
"github.com/netbirdio/netbird/client/internal/updater"
"github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/version" "github.com/netbirdio/netbird/version"
) )
@@ -89,6 +91,8 @@ type Server struct {
sleepHandler *sleephandler.SleepHandler sleepHandler *sleephandler.SleepHandler
updateManager *updater.Manager
jwtCache *jwtCache jwtCache *jwtCache
} }
@@ -135,6 +139,12 @@ func (s *Server) Start() error {
log.Warnf(errRestoreResidualState, err) log.Warnf(errRestoreResidualState, err)
} }
if s.updateManager == nil {
stateMgr := statemanager.New(s.profileManager.GetStatePath())
s.updateManager = updater.NewManager(s.statusRecorder, stateMgr)
s.updateManager.CheckUpdateSuccess(s.rootCtx)
}
// if current state contains any error, return it // if current state contains any error, return it
// in all other cases we can continue execution only if status is idle and up command was // in all other cases we can continue execution only if status is idle and up command was
// not in the progress or already successfully established connection. // not in the progress or already successfully established connection.
@@ -192,14 +202,14 @@ func (s *Server) Start() error {
s.clientRunning = true s.clientRunning = true
s.clientRunningChan = make(chan struct{}) s.clientRunningChan = make(chan struct{})
s.clientGiveUpChan = make(chan struct{}) s.clientGiveUpChan = make(chan struct{})
go s.connectWithRetryRuns(ctx, config, s.statusRecorder, false, s.clientRunningChan, s.clientGiveUpChan) go s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.clientRunningChan, s.clientGiveUpChan)
return nil return nil
} }
// connectWithRetryRuns runs the client connection with a backoff strategy where we retry the operation as additional // connectWithRetryRuns runs the client connection with a backoff strategy where we retry the operation as additional
// mechanism to keep the client connected even when the connection is lost. // mechanism to keep the client connected even when the connection is lost.
// we cancel retry if the client receive a stop or down command, or if disable auto connect is configured. // we cancel retry if the client receive a stop or down command, or if disable auto connect is configured.
func (s *Server) connectWithRetryRuns(ctx context.Context, profileConfig *profilemanager.Config, statusRecorder *peer.Status, doInitialAutoUpdate bool, runningChan chan struct{}, giveUpChan chan struct{}) { func (s *Server) connectWithRetryRuns(ctx context.Context, profileConfig *profilemanager.Config, statusRecorder *peer.Status, runningChan chan struct{}, giveUpChan chan struct{}) {
defer func() { defer func() {
s.mutex.Lock() s.mutex.Lock()
s.clientRunning = false s.clientRunning = false
@@ -207,7 +217,7 @@ func (s *Server) connectWithRetryRuns(ctx context.Context, profileConfig *profil
}() }()
if s.config.DisableAutoConnect { if s.config.DisableAutoConnect {
if err := s.connect(ctx, s.config, s.statusRecorder, doInitialAutoUpdate, runningChan); err != nil { if err := s.connect(ctx, s.config, s.statusRecorder, runningChan); err != nil {
log.Debugf("run client connection exited with error: %v", err) log.Debugf("run client connection exited with error: %v", err)
} }
log.Tracef("client connection exited") log.Tracef("client connection exited")
@@ -236,8 +246,7 @@ func (s *Server) connectWithRetryRuns(ctx context.Context, profileConfig *profil
}() }()
runOperation := func() error { runOperation := func() error {
err := s.connect(ctx, profileConfig, statusRecorder, doInitialAutoUpdate, runningChan) err := s.connect(ctx, profileConfig, statusRecorder, runningChan)
doInitialAutoUpdate = false
if err != nil { if err != nil {
log.Debugf("run client connection exited with error: %v. Will retry in the background", err) log.Debugf("run client connection exited with error: %v. Will retry in the background", err)
return err return err
@@ -717,11 +726,7 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
s.clientRunningChan = make(chan struct{}) s.clientRunningChan = make(chan struct{})
s.clientGiveUpChan = make(chan struct{}) s.clientGiveUpChan = make(chan struct{})
var doAutoUpdate bool go s.connectWithRetryRuns(ctx, s.config, s.statusRecorder, s.clientRunningChan, s.clientGiveUpChan)
if msg != nil && msg.AutoUpdate != nil && *msg.AutoUpdate {
doAutoUpdate = true
}
go s.connectWithRetryRuns(ctx, s.config, s.statusRecorder, doAutoUpdate, s.clientRunningChan, s.clientGiveUpChan)
s.mutex.Unlock() s.mutex.Unlock()
return s.waitForUp(callerCtx) return s.waitForUp(callerCtx)
@@ -1623,9 +1628,10 @@ func (s *Server) GetFeatures(ctx context.Context, msg *proto.GetFeaturesRequest)
return features, nil return features, nil
} }
func (s *Server) connect(ctx context.Context, config *profilemanager.Config, statusRecorder *peer.Status, doInitialAutoUpdate bool, runningChan chan struct{}) error { func (s *Server) connect(ctx context.Context, config *profilemanager.Config, statusRecorder *peer.Status, runningChan chan struct{}) error {
log.Tracef("running client connection") log.Tracef("running client connection")
client := internal.NewConnectClient(ctx, config, statusRecorder, doInitialAutoUpdate) client := internal.NewConnectClient(ctx, config, statusRecorder)
client.SetUpdateManager(s.updateManager)
client.SetSyncResponsePersistence(s.persistSyncResponse) client.SetSyncResponsePersistence(s.persistSyncResponse)
s.mutex.Lock() s.mutex.Lock()
@@ -1656,6 +1662,14 @@ func (s *Server) checkUpdateSettingsDisabled() bool {
return false return false
} }
func (s *Server) startUpdateManagerForGUI() {
if s.updateManager == nil {
return
}
s.updateManager.Start(s.rootCtx)
s.updateManager.NotifyUI()
}
func (s *Server) onSessionExpire() { func (s *Server) onSessionExpire() {
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
isUIActive := internal.CheckUIApp() isUIActive := internal.CheckUIApp()

View File

@@ -22,7 +22,7 @@ func newTestServer() *Server {
} }
func newDummyConnectClient(ctx context.Context) *internal.ConnectClient { func newDummyConnectClient(ctx context.Context) *internal.ConnectClient {
return internal.NewConnectClient(ctx, nil, nil, false) return internal.NewConnectClient(ctx, nil, nil)
} }
// TestConnectSetsClientWithMutex validates that connect() sets s.connectClient // TestConnectSetsClientWithMutex validates that connect() sets s.connectClient

View File

@@ -113,7 +113,7 @@ func TestConnectWithRetryRuns(t *testing.T) {
t.Setenv(maxRetryTimeVar, "5s") t.Setenv(maxRetryTimeVar, "5s")
t.Setenv(retryMultiplierVar, "1") t.Setenv(retryMultiplierVar, "1")
s.connectWithRetryRuns(ctx, config, s.statusRecorder, false, nil, nil) s.connectWithRetryRuns(ctx, config, s.statusRecorder, nil, nil)
if counter < 3 { if counter < 3 {
t.Fatalf("expected counter > 2, got %d", counter) t.Fatalf("expected counter > 2, got %d", counter)
} }

View File

@@ -0,0 +1,24 @@
package server
import (
"context"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/proto"
)
// TriggerUpdate initiates installation of the pending enforced version.
// It is called when the user clicks the install button in the UI (Mode 2 / enforced update).
func (s *Server) TriggerUpdate(ctx context.Context, _ *proto.TriggerUpdateRequest) (*proto.TriggerUpdateResponse, error) {
if s.updateManager == nil {
return &proto.TriggerUpdateResponse{Success: false, ErrorMsg: "update manager not available"}, nil
}
if err := s.updateManager.Install(ctx); err != nil {
log.Warnf("TriggerUpdate failed: %v", err)
return &proto.TriggerUpdateResponse{Success: false, ErrorMsg: err.Error()}, nil
}
return &proto.TriggerUpdateResponse{Success: true}, nil
}

View File

@@ -5,7 +5,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/updatemanager/installer" "github.com/netbirdio/netbird/client/internal/updater/installer"
"github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/proto"
) )

View File

@@ -34,7 +34,6 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
protobuf "google.golang.org/protobuf/proto"
"github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal"
@@ -308,10 +307,11 @@ type serviceClient struct {
sshJWTCacheTTL int sshJWTCacheTTL int
connected bool connected bool
update *version.Update
daemonVersion string daemonVersion string
updateIndicationLock sync.Mutex updateIndicationLock sync.Mutex
isUpdateIconActive bool isUpdateIconActive bool
isEnforcedUpdate bool
lastNotifiedVersion string
settingsEnabled bool settingsEnabled bool
profilesEnabled bool profilesEnabled bool
showNetworks bool showNetworks bool
@@ -367,7 +367,6 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient {
showAdvancedSettings: args.showSettings, showAdvancedSettings: args.showSettings,
showNetworks: args.showNetworks, showNetworks: args.showNetworks,
update: version.NewUpdateAndStart("nb/client-ui"),
} }
s.eventHandler = newEventHandler(s) s.eventHandler = newEventHandler(s)
@@ -828,7 +827,7 @@ func (s *serviceClient) handleSSOLogin(ctx context.Context, loginResp *proto.Log
return nil return nil
} }
func (s *serviceClient) menuUpClick(ctx context.Context, wannaAutoUpdate bool) error { func (s *serviceClient) menuUpClick(ctx context.Context) error {
systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting)
conn, err := s.getSrvClient(defaultFailTimeout) conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil { if err != nil {
@@ -850,9 +849,7 @@ func (s *serviceClient) menuUpClick(ctx context.Context, wannaAutoUpdate bool) e
return nil return nil
} }
if _, err := s.conn.Up(s.ctx, &proto.UpRequest{ if _, err := s.conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
AutoUpdate: protobuf.Bool(wannaAutoUpdate),
}); err != nil {
return fmt.Errorf("start connection: %w", err) return fmt.Errorf("start connection: %w", err)
} }
@@ -933,13 +930,13 @@ func (s *serviceClient) updateStatus() error {
systrayIconState = false systrayIconState = false
} }
// the updater struct notify by the upgrades available only, but if meanwhile the daemon has successfully // if the daemon version changed (e.g. after a successful update), reset the update indication
// updated must reset the mUpdate visibility state
if s.daemonVersion != status.DaemonVersion { if s.daemonVersion != status.DaemonVersion {
if s.daemonVersion != "" {
s.mUpdate.Hide() s.mUpdate.Hide()
s.isUpdateIconActive = false
}
s.daemonVersion = status.DaemonVersion s.daemonVersion = status.DaemonVersion
s.isUpdateIconActive = s.update.SetDaemonVersion(status.DaemonVersion)
if !s.isUpdateIconActive { if !s.isUpdateIconActive {
if systrayIconState { if systrayIconState {
systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected)
@@ -1091,7 +1088,6 @@ func (s *serviceClient) onTrayReady() {
// update exit node menu in case service is already connected // update exit node menu in case service is already connected
go s.updateExitNodes() go s.updateExitNodes()
s.update.SetOnUpdateListener(s.onUpdateAvailable)
go func() { go func() {
s.getSrvConfig() s.getSrvConfig()
time.Sleep(100 * time.Millisecond) // To prevent race condition caused by systray not being fully initialized and ignoring setIcon time.Sleep(100 * time.Millisecond) // To prevent race condition caused by systray not being fully initialized and ignoring setIcon
@@ -1135,6 +1131,13 @@ func (s *serviceClient) onTrayReady() {
} }
} }
}) })
s.eventManager.AddHandler(func(event *proto.SystemEvent) {
if newVersion, ok := event.Metadata["new_version_available"]; ok {
_, enforced := event.Metadata["enforced"]
log.Infof("received new_version_available event: version=%s enforced=%v", newVersion, enforced)
s.onUpdateAvailable(newVersion, enforced)
}
})
go s.eventManager.Start(s.ctx) go s.eventManager.Start(s.ctx)
go s.eventHandler.listen(s.ctx) go s.eventHandler.listen(s.ctx)
@@ -1507,10 +1510,18 @@ func protoConfigToConfig(cfg *proto.GetConfigResponse) *profilemanager.Config {
return &config return &config
} }
func (s *serviceClient) onUpdateAvailable() { func (s *serviceClient) onUpdateAvailable(newVersion string, enforced bool) {
s.updateIndicationLock.Lock() s.updateIndicationLock.Lock()
defer s.updateIndicationLock.Unlock() defer s.updateIndicationLock.Unlock()
s.isEnforcedUpdate = enforced
if enforced {
s.mUpdate.SetTitle("Install version " + newVersion)
} else {
s.lastNotifiedVersion = ""
s.mUpdate.SetTitle("Download latest version")
}
s.mUpdate.Show() s.mUpdate.Show()
s.isUpdateIconActive = true s.isUpdateIconActive = true
@@ -1519,6 +1530,11 @@ func (s *serviceClient) onUpdateAvailable() {
} else { } else {
systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected)
} }
if enforced && s.lastNotifiedVersion != newVersion {
s.lastNotifiedVersion = newVersion
s.app.SendNotification(fyne.NewNotification("Update available", "A new version "+newVersion+" is ready to install"))
}
} }
// onSessionExpire sends a notification to the user when the session expires. // onSessionExpire sends a notification to the user when the session expires.

View File

@@ -107,12 +107,7 @@ func (e *Manager) handleEvent(event *proto.SystemEvent) {
handlers := slices.Clone(e.handlers) handlers := slices.Clone(e.handlers)
e.mu.Unlock() e.mu.Unlock()
// critical events are always shown if event.UserMessage != "" && (enabled || event.Severity == proto.SystemEvent_CRITICAL) {
if !enabled && event.Severity != proto.SystemEvent_CRITICAL {
return
}
if event.UserMessage != "" {
title := e.getEventTitle(event) title := e.getEventTitle(event)
body := event.UserMessage body := event.UserMessage
id := event.Metadata["id"] id := event.Metadata["id"]

View File

@@ -82,7 +82,7 @@ func (h *eventHandler) handleConnectClick() {
go func() { go func() {
defer connectCancel() defer connectCancel()
if err := h.client.menuUpClick(connectCtx, true); err != nil { if err := h.client.menuUpClick(connectCtx); err != nil {
st, ok := status.FromError(err) st, ok := status.FromError(err)
if errors.Is(err, context.Canceled) || (ok && st.Code() == codes.Canceled) { if errors.Is(err, context.Canceled) || (ok && st.Code() == codes.Canceled) {
log.Debugf("connect operation cancelled by user") log.Debugf("connect operation cancelled by user")
@@ -211,9 +211,42 @@ func (h *eventHandler) handleGitHubClick() {
} }
func (h *eventHandler) handleUpdateClick() { func (h *eventHandler) handleUpdateClick() {
h.client.updateIndicationLock.Lock()
enforced := h.client.isEnforcedUpdate
h.client.updateIndicationLock.Unlock()
if !enforced {
if err := openURL(version.DownloadUrl()); err != nil { if err := openURL(version.DownloadUrl()); err != nil {
log.Errorf("failed to open download URL: %v", err) log.Errorf("failed to open download URL: %v", err)
} }
return
}
// prevent blocking against a busy server
h.client.mUpdate.Disable()
go func() {
defer h.client.mUpdate.Enable()
conn, err := h.client.getSrvClient(defaultFailTimeout)
if err != nil {
log.Errorf("failed to get service client for update: %v", err)
_ = openURL(version.DownloadUrl())
return
}
resp, err := conn.TriggerUpdate(h.client.ctx, &proto.TriggerUpdateRequest{})
if err != nil {
log.Errorf("TriggerUpdate failed: %v", err)
_ = openURL(version.DownloadUrl())
return
}
if !resp.Success {
log.Errorf("TriggerUpdate failed: %s", resp.ErrorMsg)
_ = openURL(version.DownloadUrl())
return
}
log.Infof("update triggered via daemon")
}()
} }
func (h *eventHandler) handleNetworksClick() { func (h *eventHandler) handleNetworksClick() {

View File

@@ -397,7 +397,7 @@ type profileMenu struct {
logoutSubItem *subItem logoutSubItem *subItem
profilesState []Profile profilesState []Profile
downClickCallback func() error downClickCallback func() error
upClickCallback func(context.Context, bool) error upClickCallback func(context.Context) error
getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error) getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error)
loadSettingsCallback func() loadSettingsCallback func()
app fyne.App app fyne.App
@@ -411,7 +411,7 @@ type newProfileMenuArgs struct {
profileMenuItem *systray.MenuItem profileMenuItem *systray.MenuItem
emailMenuItem *systray.MenuItem emailMenuItem *systray.MenuItem
downClickCallback func() error downClickCallback func() error
upClickCallback func(context.Context, bool) error upClickCallback func(context.Context) error
getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error) getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error)
loadSettingsCallback func() loadSettingsCallback func()
app fyne.App app fyne.App
@@ -579,7 +579,7 @@ func (p *profileMenu) refresh() {
connectCtx, connectCancel := context.WithCancel(p.ctx) connectCtx, connectCancel := context.WithCancel(p.ctx)
p.serviceClient.connectCancel = connectCancel p.serviceClient.connectCancel = connectCancel
if err := p.upClickCallback(connectCtx, false); err != nil { if err := p.upClickCallback(connectCtx); err != nil {
log.Errorf("failed to handle up click after switching profile: %v", err) log.Errorf("failed to handle up click after switching profile: %v", err)
} }

View File

@@ -267,7 +267,7 @@ func (s *serviceClient) showQuickActionsUI() {
connCmd := connectCommand{ connCmd := connectCommand{
connectClient: func() error { connectClient: func() error {
return s.menuUpClick(s.ctx, false) return s.menuUpClick(s.ctx)
}, },
} }

View File

@@ -108,6 +108,7 @@ func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, set
LazyConnectionEnabled: settings.LazyConnectionEnabled, LazyConnectionEnabled: settings.LazyConnectionEnabled,
AutoUpdate: &proto.AutoUpdateSettings{ AutoUpdate: &proto.AutoUpdateSettings{
Version: settings.AutoUpdateVersion, Version: settings.AutoUpdateVersion,
AlwaysUpdate: settings.AutoUpdateAlways,
}, },
} }
} }

View File

@@ -335,7 +335,8 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
if oldSettings.RoutingPeerDNSResolutionEnabled != newSettings.RoutingPeerDNSResolutionEnabled || if oldSettings.RoutingPeerDNSResolutionEnabled != newSettings.RoutingPeerDNSResolutionEnabled ||
oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled || oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled ||
oldSettings.DNSDomain != newSettings.DNSDomain || oldSettings.DNSDomain != newSettings.DNSDomain ||
oldSettings.AutoUpdateVersion != newSettings.AutoUpdateVersion { oldSettings.AutoUpdateVersion != newSettings.AutoUpdateVersion ||
oldSettings.AutoUpdateAlways != newSettings.AutoUpdateAlways {
updateAccountPeers = true updateAccountPeers = true
} }
@@ -376,6 +377,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
am.handlePeerLoginExpirationSettings(ctx, oldSettings, newSettings, userID, accountID) am.handlePeerLoginExpirationSettings(ctx, oldSettings, newSettings, userID, accountID)
am.handleGroupsPropagationSettings(ctx, oldSettings, newSettings, userID, accountID) am.handleGroupsPropagationSettings(ctx, oldSettings, newSettings, userID, accountID)
am.handleAutoUpdateVersionSettings(ctx, oldSettings, newSettings, userID, accountID) am.handleAutoUpdateVersionSettings(ctx, oldSettings, newSettings, userID, accountID)
am.handleAutoUpdateAlwaysSettings(ctx, oldSettings, newSettings, userID, accountID)
am.handlePeerExposeSettings(ctx, oldSettings, newSettings, userID, accountID) am.handlePeerExposeSettings(ctx, oldSettings, newSettings, userID, accountID)
if err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil { if err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil {
return nil, err return nil, err
@@ -493,6 +495,16 @@ func (am *DefaultAccountManager) handleAutoUpdateVersionSettings(ctx context.Con
} }
} }
func (am *DefaultAccountManager) handleAutoUpdateAlwaysSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) {
if oldSettings.AutoUpdateAlways != newSettings.AutoUpdateAlways {
if newSettings.AutoUpdateAlways {
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountAutoUpdateAlwaysEnabled, nil)
} else {
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountAutoUpdateAlwaysDisabled, nil)
}
}
}
func (am *DefaultAccountManager) handlePeerExposeSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) { func (am *DefaultAccountManager) handlePeerExposeSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) {
oldEnabled := oldSettings.PeerExposeEnabled oldEnabled := oldSettings.PeerExposeEnabled
newEnabled := newSettings.PeerExposeEnabled newEnabled := newSettings.PeerExposeEnabled

View File

@@ -220,6 +220,11 @@ const (
// AccountPeerExposeDisabled indicates that a user disabled peer expose for the account // AccountPeerExposeDisabled indicates that a user disabled peer expose for the account
AccountPeerExposeDisabled Activity = 115 AccountPeerExposeDisabled Activity = 115
// AccountAutoUpdateAlwaysEnabled indicates that a user enabled always auto-update for the account
AccountAutoUpdateAlwaysEnabled Activity = 116
// AccountAutoUpdateAlwaysDisabled indicates that a user disabled always auto-update for the account
AccountAutoUpdateAlwaysDisabled Activity = 117
// DomainAdded indicates that a user added a custom domain // DomainAdded indicates that a user added a custom domain
DomainAdded Activity = 118 DomainAdded Activity = 118
// DomainDeleted indicates that a user deleted a custom domain // DomainDeleted indicates that a user deleted a custom domain
@@ -339,6 +344,8 @@ var activityMap = map[Activity]Code{
UserCreated: {"User created", "user.create"}, UserCreated: {"User created", "user.create"},
AccountAutoUpdateVersionUpdated: {"Account AutoUpdate Version updated", "account.settings.auto.version.update"}, AccountAutoUpdateVersionUpdated: {"Account AutoUpdate Version updated", "account.settings.auto.version.update"},
AccountAutoUpdateAlwaysEnabled: {"Account auto-update always enabled", "account.setting.auto.update.always.enable"},
AccountAutoUpdateAlwaysDisabled: {"Account auto-update always disabled", "account.setting.auto.update.always.disable"},
IdentityProviderCreated: {"Identity provider created", "identityprovider.create"}, IdentityProviderCreated: {"Identity provider created", "identityprovider.create"},
IdentityProviderUpdated: {"Identity provider updated", "identityprovider.update"}, IdentityProviderUpdated: {"Identity provider updated", "identityprovider.update"},

View File

@@ -225,6 +225,9 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS
return nil, fmt.Errorf("invalid AutoUpdateVersion") return nil, fmt.Errorf("invalid AutoUpdateVersion")
} }
} }
if req.Settings.AutoUpdateAlways != nil {
returnSettings.AutoUpdateAlways = *req.Settings.AutoUpdateAlways
}
return returnSettings, nil return returnSettings, nil
} }
@@ -348,6 +351,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
LazyConnectionEnabled: &settings.LazyConnectionEnabled, LazyConnectionEnabled: &settings.LazyConnectionEnabled,
DnsDomain: &settings.DNSDomain, DnsDomain: &settings.DNSDomain,
AutoUpdateVersion: &settings.AutoUpdateVersion, AutoUpdateVersion: &settings.AutoUpdateVersion,
AutoUpdateAlways: &settings.AutoUpdateAlways,
EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled, EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled,
LocalAuthDisabled: &settings.LocalAuthDisabled, LocalAuthDisabled: &settings.LocalAuthDisabled,
} }

View File

@@ -121,6 +121,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
RoutingPeerDnsResolutionEnabled: br(false), RoutingPeerDnsResolutionEnabled: br(false),
LazyConnectionEnabled: br(false), LazyConnectionEnabled: br(false),
DnsDomain: sr(""), DnsDomain: sr(""),
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""), AutoUpdateVersion: sr(""),
EmbeddedIdpEnabled: br(false), EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false), LocalAuthDisabled: br(false),
@@ -146,6 +147,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
RoutingPeerDnsResolutionEnabled: br(false), RoutingPeerDnsResolutionEnabled: br(false),
LazyConnectionEnabled: br(false), LazyConnectionEnabled: br(false),
DnsDomain: sr(""), DnsDomain: sr(""),
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""), AutoUpdateVersion: sr(""),
EmbeddedIdpEnabled: br(false), EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false), LocalAuthDisabled: br(false),
@@ -171,6 +173,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
RoutingPeerDnsResolutionEnabled: br(false), RoutingPeerDnsResolutionEnabled: br(false),
LazyConnectionEnabled: br(false), LazyConnectionEnabled: br(false),
DnsDomain: sr(""), DnsDomain: sr(""),
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr("latest"), AutoUpdateVersion: sr("latest"),
EmbeddedIdpEnabled: br(false), EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false), LocalAuthDisabled: br(false),
@@ -196,6 +199,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
RoutingPeerDnsResolutionEnabled: br(false), RoutingPeerDnsResolutionEnabled: br(false),
LazyConnectionEnabled: br(false), LazyConnectionEnabled: br(false),
DnsDomain: sr(""), DnsDomain: sr(""),
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""), AutoUpdateVersion: sr(""),
EmbeddedIdpEnabled: br(false), EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false), LocalAuthDisabled: br(false),
@@ -221,6 +225,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
RoutingPeerDnsResolutionEnabled: br(false), RoutingPeerDnsResolutionEnabled: br(false),
LazyConnectionEnabled: br(false), LazyConnectionEnabled: br(false),
DnsDomain: sr(""), DnsDomain: sr(""),
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""), AutoUpdateVersion: sr(""),
EmbeddedIdpEnabled: br(false), EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false), LocalAuthDisabled: br(false),
@@ -246,6 +251,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
RoutingPeerDnsResolutionEnabled: br(false), RoutingPeerDnsResolutionEnabled: br(false),
LazyConnectionEnabled: br(false), LazyConnectionEnabled: br(false),
DnsDomain: sr(""), DnsDomain: sr(""),
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""), AutoUpdateVersion: sr(""),
EmbeddedIdpEnabled: br(false), EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false), LocalAuthDisabled: br(false),

View File

@@ -61,6 +61,10 @@ type Settings struct {
// AutoUpdateVersion client auto-update version // AutoUpdateVersion client auto-update version
AutoUpdateVersion string `gorm:"default:'disabled'"` AutoUpdateVersion string `gorm:"default:'disabled'"`
// AutoUpdateAlways when true, updates are installed automatically in the background;
// when false, updates require user interaction from the UI
AutoUpdateAlways bool `gorm:"default:false"`
// EmbeddedIdpEnabled indicates if the embedded identity provider is enabled. // EmbeddedIdpEnabled indicates if the embedded identity provider is enabled.
// This is a runtime-only field, not stored in the database. // This is a runtime-only field, not stored in the database.
EmbeddedIdpEnabled bool `gorm:"-"` EmbeddedIdpEnabled bool `gorm:"-"`
@@ -91,6 +95,7 @@ func (s *Settings) Copy() *Settings {
DNSDomain: s.DNSDomain, DNSDomain: s.DNSDomain,
NetworkRange: s.NetworkRange, NetworkRange: s.NetworkRange,
AutoUpdateVersion: s.AutoUpdateVersion, AutoUpdateVersion: s.AutoUpdateVersion,
AutoUpdateAlways: s.AutoUpdateAlways,
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled, EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
LocalAuthDisabled: s.LocalAuthDisabled, LocalAuthDisabled: s.LocalAuthDisabled,
} }

View File

@@ -347,6 +347,10 @@ components:
description: Set Clients auto-update version. "latest", "disabled", or a specific version (e.g "0.50.1") description: Set Clients auto-update version. "latest", "disabled", or a specific version (e.g "0.50.1")
type: string type: string
example: "0.51.2" example: "0.51.2"
auto_update_always:
description: When true, updates are installed automatically in the background. When false, updates require user interaction from the UI.
type: boolean
example: false
embedded_idp_enabled: embedded_idp_enabled:
description: Indicates whether the embedded identity provider (Dex) is enabled for this account. This is a read-only field. description: Indicates whether the embedded identity provider (Dex) is enabled for this account. This is a read-only field.
type: boolean type: boolean

View File

@@ -1307,6 +1307,9 @@ type AccountRequest struct {
// AccountSettings defines model for AccountSettings. // AccountSettings defines model for AccountSettings.
type AccountSettings struct { type AccountSettings struct {
// AutoUpdateAlways When true, updates are installed automatically in the background. When false, updates require user interaction from the UI.
AutoUpdateAlways *bool `json:"auto_update_always,omitempty"`
// AutoUpdateVersion Set Clients auto-update version. "latest", "disabled", or a specific version (e.g "0.50.1") // AutoUpdateVersion Set Clients auto-update version. "latest", "disabled", or a specific version (e.g "0.50.1")
AutoUpdateVersion *string `json:"auto_update_version,omitempty"` AutoUpdateVersion *string `json:"auto_update_version,omitempty"`

View File

@@ -340,8 +340,8 @@ message PeerConfig {
message AutoUpdateSettings { message AutoUpdateSettings {
string version = 1; string version = 1;
/* /*
alwaysUpdate = true → Updates happen automatically in the background alwaysUpdate = true → Updates are installed automatically in the background
alwaysUpdate = false → Updates only happen when triggered by a peer connection alwaysUpdate = false → Updates require user interaction from the UI
*/ */
bool alwaysUpdate = 2; bool alwaysUpdate = 2;
} }