mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-20 23:41:28 +02:00
Feature: Auto-update client
This commit is contained in:
@@ -50,6 +50,7 @@ import (
|
||||
"github.com/netbirdio/netbird/client/internal/routemanager"
|
||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||
"github.com/netbirdio/netbird/client/internal/updatemanager"
|
||||
cProto "github.com/netbirdio/netbird/client/proto"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group"
|
||||
@@ -198,6 +199,9 @@ type Engine struct {
|
||||
latestSyncResponse *mgmProto.SyncResponse
|
||||
connSemaphore *semaphoregroup.SemaphoreGroup
|
||||
flowManager nftypes.FlowManager
|
||||
|
||||
// auto-update
|
||||
updateManager *updatemanager.UpdateManager
|
||||
}
|
||||
|
||||
// Peer is an instance of the Connection Peer
|
||||
@@ -240,6 +244,7 @@ func NewEngine(
|
||||
statusRecorder: statusRecorder,
|
||||
checks: checks,
|
||||
connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit),
|
||||
updateManager: updatemanager.NewUpdateManager(clientCtx, statusRecorder),
|
||||
}
|
||||
|
||||
sm := profilemanager.NewServiceManager("")
|
||||
@@ -306,6 +311,10 @@ func (e *Engine) Stop() error {
|
||||
e.srWatcher.Close()
|
||||
}
|
||||
|
||||
if e.updateManager != nil {
|
||||
e.updateManager.Stop()
|
||||
}
|
||||
|
||||
e.statusRecorder.ReplaceOfflinePeers([]peer.State{})
|
||||
e.statusRecorder.UpdateDNSStates([]peer.NSGroupState{})
|
||||
e.statusRecorder.UpdateRelayStates([]relay.ProbeResult{})
|
||||
@@ -696,6 +705,9 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
||||
e.syncMsgMux.Lock()
|
||||
defer e.syncMsgMux.Unlock()
|
||||
|
||||
if update.GetAutoUpdateVersion() != "skip" {
|
||||
e.updateManager.SetVersion(update.GetAutoUpdateVersion())
|
||||
}
|
||||
if update.GetNetbirdConfig() != nil {
|
||||
wCfg := update.GetNetbirdConfig()
|
||||
err := e.updateTURNs(wCfg.GetTurns())
|
||||
|
||||
183
client/internal/updatemanager/manager.go
Normal file
183
client/internal/updatemanager/manager.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package updatemanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
v "github.com/hashicorp/go-version"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/peer"
|
||||
cProto "github.com/netbirdio/netbird/client/proto"
|
||||
"github.com/netbirdio/netbird/version"
|
||||
)
|
||||
|
||||
const (
|
||||
latestVersion = "latest"
|
||||
disableAutoUpdate = "disabled"
|
||||
unknownVersion = "Unknown"
|
||||
)
|
||||
|
||||
type UpdateManager struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
version string
|
||||
latestVersion string
|
||||
update *version.Update
|
||||
lastTrigger time.Time
|
||||
statusRecorder *peer.Status
|
||||
mutex sync.Mutex
|
||||
waitGroup sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewUpdateManager(ctx context.Context, statusRecorder *peer.Status) *UpdateManager {
|
||||
update := version.NewUpdate("nb/client")
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
manager := &UpdateManager{
|
||||
update: update,
|
||||
lastTrigger: time.Now().Add(-10 * time.Minute),
|
||||
statusRecorder: statusRecorder,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
version: disableAutoUpdate,
|
||||
latestVersion: unknownVersion,
|
||||
}
|
||||
update.SetDaemonVersion(version.NetbirdVersion())
|
||||
update.SetOnUpdateListener(manager.Updated)
|
||||
return manager
|
||||
}
|
||||
|
||||
func (u *UpdateManager) SetVersion(v string) {
|
||||
u.mutex.Lock()
|
||||
if u.version != v {
|
||||
log.Tracef("Auto-update version set to %s", v)
|
||||
u.version = v
|
||||
u.mutex.Unlock()
|
||||
go u.Updated("N/A")
|
||||
} else {
|
||||
u.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UpdateManager) Stop() {
|
||||
u.update.StopWatch()
|
||||
u.cancel()
|
||||
u.waitGroup.Wait()
|
||||
}
|
||||
|
||||
func (u *UpdateManager) Updated(latestVersion string) {
|
||||
u.waitGroup.Add(1)
|
||||
defer u.waitGroup.Done()
|
||||
u.mutex.Lock()
|
||||
defer u.mutex.Unlock()
|
||||
select {
|
||||
case <-u.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
if latestVersion != "N/A" {
|
||||
u.latestVersion = latestVersion
|
||||
}
|
||||
ctx, cancel := context.WithDeadline(u.ctx, time.Now().Add(time.Minute))
|
||||
defer cancel()
|
||||
u.CheckForUpdates(ctx)
|
||||
}
|
||||
|
||||
func (u *UpdateManager) CheckForUpdates(ctx context.Context) {
|
||||
if u.version == disableAutoUpdate {
|
||||
log.Trace("Skipped checking for updates, auto-update is disabled")
|
||||
return
|
||||
}
|
||||
currentVersionString := version.NetbirdVersion()
|
||||
updateVersionString := u.version
|
||||
if updateVersionString == latestVersion || updateVersionString == "" {
|
||||
if u.latestVersion == unknownVersion {
|
||||
log.Tracef("Latest version not fetched yet")
|
||||
return
|
||||
}
|
||||
updateVersionString = u.latestVersion
|
||||
}
|
||||
currentVersion, err := v.NewVersion(currentVersionString)
|
||||
if err != nil {
|
||||
log.Errorf("Error checking for update, error parsing version `%s`: %v", currentVersionString, err)
|
||||
return
|
||||
}
|
||||
updateVersion, err := v.NewVersion(updateVersionString)
|
||||
if err != nil {
|
||||
log.Errorf("Error checking for update, error parsing version `%s`: %v", updateVersionString, err)
|
||||
return
|
||||
}
|
||||
if currentVersion.LessThan(updateVersion) {
|
||||
if u.lastTrigger.Add(5 * time.Minute).Before(time.Now()) {
|
||||
u.lastTrigger = time.Now()
|
||||
log.Debugf("Auto-update triggered, current version: %s, target version: %s", currentVersionString, updateVersionString)
|
||||
u.statusRecorder.PublishEvent(
|
||||
cProto.SystemEvent_INFO,
|
||||
cProto.SystemEvent_SYSTEM,
|
||||
"Automatically updating client",
|
||||
"Your client version is older than auto-update version set in Management, updating client now.",
|
||||
nil,
|
||||
)
|
||||
err = u.triggerUpdate(ctx, updateVersionString)
|
||||
if err != nil {
|
||||
log.Errorf("Error triggering auto-update: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Debugf("Current version (%s) is equal to or higher than auto-update version (%s)", currentVersionString, updateVersionString)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFileToTemporaryDir(ctx context.Context, fileURL string) (string, error) { //nolint:unused
|
||||
tempDir, err := os.MkdirTemp("", "netbird-installer-*")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating temporary directory: %w", err)
|
||||
}
|
||||
fileNameParts := strings.Split(fileURL, "/")
|
||||
out, err := os.Create(filepath.Join(tempDir, fileNameParts[len(fileNameParts)-1]))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating temporary file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := out.Close(); err != nil {
|
||||
log.Errorf("Error closing temporary file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", fileURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating file download request: %w", err)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error downloading file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Errorf("Error closing response body: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error downloading file: %w", err)
|
||||
}
|
||||
|
||||
log.Tracef("Downloaded update file to %s", out.Name())
|
||||
|
||||
return out.Name(), nil
|
||||
}
|
||||
|
||||
func urlWithVersionArch(url, version string) string { //nolint:unused
|
||||
url = strings.ReplaceAll(url, "%version", version)
|
||||
url = strings.ReplaceAll(url, "%arch", runtime.GOARCH)
|
||||
return url
|
||||
}
|
||||
112
client/internal/updatemanager/update_darwin.go
Normal file
112
client/internal/updatemanager/update_darwin.go
Normal file
@@ -0,0 +1,112 @@
|
||||
//go:build darwin
|
||||
|
||||
package updatemanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
pkgDownloadURL = "https://github.com/netbirdio/netbird/releases/download/v%version/netbird_%version_darwin_%arch.pkg"
|
||||
)
|
||||
|
||||
func (u *UpdateManager) triggerUpdate(ctx context.Context, targetVersion string) error {
|
||||
cmd := exec.CommandContext(ctx, "pkgutil", "--pkg-info", "io.netbird.client")
|
||||
outBytes, err := cmd.Output()
|
||||
if err != nil && cmd.ProcessState.ExitCode() == 1 {
|
||||
// Not installed using pkg file, thus installed using Homebrew
|
||||
|
||||
return u.updateHomeBrew(ctx)
|
||||
}
|
||||
// Installed using pkg file
|
||||
path, err := downloadFileToTemporaryDir(ctx, urlWithVersionArch(pkgDownloadURL, targetVersion))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error downloading update file: %w", err)
|
||||
}
|
||||
|
||||
volume := "/"
|
||||
for _, v := range strings.Split(string(outBytes), "\n") {
|
||||
trimmed := strings.TrimSpace(v)
|
||||
if strings.HasPrefix(trimmed, "volume: ") {
|
||||
volume = strings.Split(trimmed, ": ")[1]
|
||||
}
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "installer", "-pkg", path, "-target", volume)
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running pkg file: %w", err)
|
||||
}
|
||||
err = cmd.Process.Release()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *UpdateManager) updateHomeBrew(ctx context.Context) error {
|
||||
// Homebrew must be run as a non-root user
|
||||
// To find out which user installed NetBird using HomeBrew we can check the owner of our brew tap directory
|
||||
fileInfo, err := os.Stat("/opt/homebrew/Library/Taps/netbirdio/homebrew-tap/")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting homebrew installation path info: %w", err)
|
||||
}
|
||||
|
||||
fileSysInfo, ok := fileInfo.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return fmt.Errorf("error checking file owner, sysInfo type is %T not *syscall.Stat_t", fileInfo.Sys())
|
||||
}
|
||||
|
||||
// Get username from UID
|
||||
installer, err := user.LookupId(fmt.Sprintf("%d", fileSysInfo.Uid))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error looking up brew installer user: %w", err)
|
||||
}
|
||||
userName := installer.Name
|
||||
// Get user HOME, required for brew to run correctly
|
||||
// https://github.com/Homebrew/brew/issues/15833
|
||||
homeDir := installer.HomeDir
|
||||
// Homebrew does not support installing specific versions
|
||||
// Thus it will always update to latest and ignore targetVersion
|
||||
upgradeArgs := []string{"-u", userName, "/opt/homebrew/bin/brew", "upgrade", "netbirdio/tap/netbird"}
|
||||
// Check if netbird-ui is installed
|
||||
cmd := exec.CommandContext(ctx, "brew", "info", "--json", "netbirdio/tap/netbird-ui")
|
||||
err = cmd.Run()
|
||||
if err == nil {
|
||||
// netbird-ui is installed
|
||||
upgradeArgs = append(upgradeArgs, "netbirdio/tap/netbird-ui")
|
||||
}
|
||||
cmd = exec.CommandContext(ctx, "sudo", upgradeArgs...)
|
||||
cmd.Env = append(cmd.Env, "HOME="+homeDir)
|
||||
|
||||
// Homebrew upgrade doesn't restart the client on its own
|
||||
// So we have to wait for it to finish running and ensure it's done
|
||||
// And then basically restart the netbird service
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running brew upgrade: %w", err)
|
||||
}
|
||||
|
||||
currentPID := os.Getpid()
|
||||
|
||||
// Restart netbird service after the fact
|
||||
// This is a workaround since attempting to restart using launchctl will kill the service and die before starting
|
||||
// the service again as it's a child process
|
||||
// using SIGTERM should ensure a clean shutdown
|
||||
process, err := os.FindProcess(currentPID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding current process: %w", err)
|
||||
}
|
||||
err = process.Signal(syscall.SIGTERM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending SIGTERM to current process: %w", err)
|
||||
}
|
||||
// We're dying now, which should restart us
|
||||
|
||||
return nil
|
||||
}
|
||||
10
client/internal/updatemanager/update_freebsd.go
Normal file
10
client/internal/updatemanager/update_freebsd.go
Normal file
@@ -0,0 +1,10 @@
|
||||
//go:build freebsd
|
||||
|
||||
package updatemanager
|
||||
|
||||
import "context"
|
||||
|
||||
func (u *UpdateManager) triggerUpdate(ctx context.Context, targetVersion string) error {
|
||||
// TODO: Implement
|
||||
return nil
|
||||
}
|
||||
10
client/internal/updatemanager/update_linux.go
Normal file
10
client/internal/updatemanager/update_linux.go
Normal file
@@ -0,0 +1,10 @@
|
||||
//go:build linux
|
||||
|
||||
package updatemanager
|
||||
|
||||
import "context"
|
||||
|
||||
func (u *UpdateManager) triggerUpdate(ctx context.Context, targetVersion string) error {
|
||||
// TODO: Implement
|
||||
return nil
|
||||
}
|
||||
77
client/internal/updatemanager/update_windows.go
Normal file
77
client/internal/updatemanager/update_windows.go
Normal file
@@ -0,0 +1,77 @@
|
||||
//go:build windows
|
||||
|
||||
package updatemanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
msiDownloadURL = "https://github.com/netbirdio/netbird/releases/download/v%version/netbird_installer_%version_windows_%arch.msi"
|
||||
exeDownloadURL = "https://github.com/netbirdio/netbird/releases/download/v%version/netbird_installer_%version_windows_%arch.exe"
|
||||
uninstallKeyPath64 = `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Netbird`
|
||||
uninstallKeyPath32 = `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Netbird`
|
||||
)
|
||||
|
||||
func installationMethod() string {
|
||||
k, err := registry.OpenKey(registry.LOCAL_MACHINE, uninstallKeyPath64, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
k, err = registry.OpenKey(registry.LOCAL_MACHINE, uninstallKeyPath32, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "MSI"
|
||||
} else {
|
||||
err = k.Close()
|
||||
if err != nil {
|
||||
log.Warnf("Error closing registry key: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = k.Close()
|
||||
if err != nil {
|
||||
log.Warnf("Error closing registry key: %v", err)
|
||||
}
|
||||
}
|
||||
return "EXE"
|
||||
}
|
||||
|
||||
func (u *UpdateManager) updateMSI(ctx context.Context, targetVersion string) error {
|
||||
path, err := downloadFileToTemporaryDir(ctx, urlWithVersionArch(msiDownloadURL, targetVersion))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "msiexec", "/quiet", "/i", path)
|
||||
err = cmd.Run()
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *UpdateManager) updateEXE(ctx context.Context, targetVersion string) error {
|
||||
path, err := downloadFileToTemporaryDir(ctx, urlWithVersionArch(exeDownloadURL, targetVersion))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, path, "/S")
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = cmd.Process.Release()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *UpdateManager) triggerUpdate(ctx context.Context, targetVersion string) error {
|
||||
switch installationMethod() {
|
||||
case "EXE":
|
||||
return u.updateEXE(ctx, targetVersion)
|
||||
case "MSI":
|
||||
return u.updateMSI(ctx, targetVersion)
|
||||
default:
|
||||
return fmt.Errorf("unsupported installation method: %s", installationMethod())
|
||||
}
|
||||
}
|
||||
@@ -1213,7 +1213,7 @@ func protoConfigToConfig(cfg *proto.GetConfigResponse) *profilemanager.Config {
|
||||
return &config
|
||||
}
|
||||
|
||||
func (s *serviceClient) onUpdateAvailable() {
|
||||
func (s *serviceClient) onUpdateAvailable(_ string) {
|
||||
s.updateIndicationLock.Lock()
|
||||
defer s.updateIndicationLock.Unlock()
|
||||
|
||||
|
||||
@@ -182,8 +182,8 @@ func (s *BaseServer) Start(ctx context.Context) error {
|
||||
|
||||
s.update = version.NewUpdate("nb/management")
|
||||
s.update.SetDaemonVersion(version.NetbirdVersion())
|
||||
s.update.SetOnUpdateListener(func() {
|
||||
log.WithContext(ctx).Infof("your management version, \"%s\", is outdated, a new management version is available. Learn more here: https://github.com/netbirdio/netbird/releases", version.NetbirdVersion())
|
||||
s.update.SetOnUpdateListener(func(newVersion string) {
|
||||
log.WithContext(ctx).Infof("your management version, \"%s\", is outdated, a new management version (%s) is available. Learn more here: https://github.com/netbirdio/netbird/releases", version.NetbirdVersion(), newVersion)
|
||||
})
|
||||
|
||||
return nil
|
||||
|
||||
@@ -334,7 +334,8 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
|
||||
|
||||
if oldSettings.RoutingPeerDNSResolutionEnabled != newSettings.RoutingPeerDNSResolutionEnabled ||
|
||||
oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled ||
|
||||
oldSettings.DNSDomain != newSettings.DNSDomain {
|
||||
oldSettings.DNSDomain != newSettings.DNSDomain ||
|
||||
oldSettings.AutoUpdateVersion != newSettings.AutoUpdateVersion {
|
||||
updateAccountPeers = true
|
||||
}
|
||||
|
||||
@@ -370,6 +371,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
|
||||
am.handleLazyConnectionSettings(ctx, oldSettings, newSettings, userID, accountID)
|
||||
am.handlePeerLoginExpirationSettings(ctx, oldSettings, newSettings, userID, accountID)
|
||||
am.handleGroupsPropagationSettings(ctx, oldSettings, newSettings, userID, accountID)
|
||||
am.handleAutoUpdateVersionSettings(ctx, oldSettings, newSettings, userID, accountID)
|
||||
if err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -471,6 +473,15 @@ func (am *DefaultAccountManager) handleGroupsPropagationSettings(ctx context.Con
|
||||
}
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) handleAutoUpdateVersionSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) {
|
||||
if oldSettings.AutoUpdateVersion != newSettings.AutoUpdateVersion {
|
||||
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountAutoUpdateVersionUpdated, map[string]any{
|
||||
"old_value": oldSettings.AutoUpdateVersion,
|
||||
"new_value": newSettings.AutoUpdateVersion,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) handleInactivityExpirationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) error {
|
||||
if newSettings.PeerInactivityExpirationEnabled {
|
||||
if oldSettings.PeerInactivityExpiration != newSettings.PeerInactivityExpiration {
|
||||
|
||||
@@ -178,6 +178,8 @@ const (
|
||||
AccountNetworkRangeUpdated Activity = 87
|
||||
PeerIPUpdated Activity = 88
|
||||
|
||||
AccountAutoUpdateVersionUpdated Activity = 89
|
||||
|
||||
AccountDeleted Activity = 99999
|
||||
)
|
||||
|
||||
@@ -284,6 +286,8 @@ var activityMap = map[Activity]Code{
|
||||
AccountNetworkRangeUpdated: {"Account network range updated", "account.network.range.update"},
|
||||
|
||||
PeerIPUpdated: {"Peer IP updated", "peer.ip.update"},
|
||||
|
||||
AccountAutoUpdateVersionUpdated: {"Account AutoUpdate Version updated", "account.settings.auto.version.update"},
|
||||
}
|
||||
|
||||
// StringCode returns a string code of the activity
|
||||
|
||||
@@ -721,7 +721,8 @@ func toSyncResponse(ctx context.Context, config *nbconfig.Config, peer *nbpeer.P
|
||||
Routes: toProtocolRoutes(networkMap.Routes),
|
||||
DNSConfig: toProtocolDNSConfig(networkMap.DNSConfig, dnsCache),
|
||||
},
|
||||
Checks: toProtocolChecks(ctx, checks),
|
||||
Checks: toProtocolChecks(ctx, checks),
|
||||
AutoUpdateVersion: settings.AutoUpdateVersion,
|
||||
}
|
||||
|
||||
nbConfig := toNetbirdConfig(config, turnCredentials, relayCredentials, extraSettings)
|
||||
|
||||
@@ -3,19 +3,21 @@ package accounts
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
goversion "github.com/hashicorp/go-version"
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
"github.com/netbirdio/netbird/management/server/settings"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||
"github.com/netbirdio/netbird/management/server/settings"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -162,6 +164,60 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
util.WriteJSONObject(r.Context(), w, []*api.Account{resp})
|
||||
}
|
||||
|
||||
func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJSONRequestBody) (*types.Settings, error) {
|
||||
returnSettings := &types.Settings{
|
||||
PeerLoginExpirationEnabled: req.Settings.PeerLoginExpirationEnabled,
|
||||
PeerLoginExpiration: time.Duration(float64(time.Second.Nanoseconds()) * float64(req.Settings.PeerLoginExpiration)),
|
||||
RegularUsersViewBlocked: req.Settings.RegularUsersViewBlocked,
|
||||
|
||||
PeerInactivityExpirationEnabled: req.Settings.PeerInactivityExpirationEnabled,
|
||||
PeerInactivityExpiration: time.Duration(float64(time.Second.Nanoseconds()) * float64(req.Settings.PeerInactivityExpiration)),
|
||||
}
|
||||
|
||||
if req.Settings.Extra != nil {
|
||||
returnSettings.Extra = &types.ExtraSettings{
|
||||
PeerApprovalEnabled: req.Settings.Extra.PeerApprovalEnabled,
|
||||
FlowEnabled: req.Settings.Extra.NetworkTrafficLogsEnabled,
|
||||
FlowGroups: req.Settings.Extra.NetworkTrafficLogsGroups,
|
||||
FlowPacketCounterEnabled: req.Settings.Extra.NetworkTrafficPacketCounterEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
if req.Settings.JwtGroupsEnabled != nil {
|
||||
returnSettings.JWTGroupsEnabled = *req.Settings.JwtGroupsEnabled
|
||||
}
|
||||
if req.Settings.GroupsPropagationEnabled != nil {
|
||||
returnSettings.GroupsPropagationEnabled = *req.Settings.GroupsPropagationEnabled
|
||||
}
|
||||
if req.Settings.JwtGroupsClaimName != nil {
|
||||
returnSettings.JWTGroupsClaimName = *req.Settings.JwtGroupsClaimName
|
||||
}
|
||||
if req.Settings.JwtAllowGroups != nil {
|
||||
returnSettings.JWTAllowGroups = *req.Settings.JwtAllowGroups
|
||||
}
|
||||
if req.Settings.RoutingPeerDnsResolutionEnabled != nil {
|
||||
returnSettings.RoutingPeerDNSResolutionEnabled = *req.Settings.RoutingPeerDnsResolutionEnabled
|
||||
}
|
||||
if req.Settings.DnsDomain != nil {
|
||||
returnSettings.DNSDomain = *req.Settings.DnsDomain
|
||||
}
|
||||
if req.Settings.LazyConnectionEnabled != nil {
|
||||
returnSettings.LazyConnectionEnabled = *req.Settings.LazyConnectionEnabled
|
||||
}
|
||||
if req.Settings.AutoUpdateVersion != nil {
|
||||
_, err := goversion.NewSemver(*req.Settings.AutoUpdateVersion)
|
||||
if *req.Settings.AutoUpdateVersion == "latest" ||
|
||||
*req.Settings.AutoUpdateVersion == "disabled" ||
|
||||
err == nil {
|
||||
returnSettings.AutoUpdateVersion = *req.Settings.AutoUpdateVersion
|
||||
} else if *req.Settings.AutoUpdateVersion != "" {
|
||||
return nil, fmt.Errorf("invalid AutoUpdateVersion")
|
||||
}
|
||||
}
|
||||
|
||||
return returnSettings, nil
|
||||
}
|
||||
|
||||
// updateAccount is HTTP PUT handler that updates the provided account. Updates only account settings (server.Settings)
|
||||
func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
@@ -186,44 +242,9 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
settings := &types.Settings{
|
||||
PeerLoginExpirationEnabled: req.Settings.PeerLoginExpirationEnabled,
|
||||
PeerLoginExpiration: time.Duration(float64(time.Second.Nanoseconds()) * float64(req.Settings.PeerLoginExpiration)),
|
||||
RegularUsersViewBlocked: req.Settings.RegularUsersViewBlocked,
|
||||
|
||||
PeerInactivityExpirationEnabled: req.Settings.PeerInactivityExpirationEnabled,
|
||||
PeerInactivityExpiration: time.Duration(float64(time.Second.Nanoseconds()) * float64(req.Settings.PeerInactivityExpiration)),
|
||||
}
|
||||
|
||||
if req.Settings.Extra != nil {
|
||||
settings.Extra = &types.ExtraSettings{
|
||||
PeerApprovalEnabled: req.Settings.Extra.PeerApprovalEnabled,
|
||||
FlowEnabled: req.Settings.Extra.NetworkTrafficLogsEnabled,
|
||||
FlowGroups: req.Settings.Extra.NetworkTrafficLogsGroups,
|
||||
FlowPacketCounterEnabled: req.Settings.Extra.NetworkTrafficPacketCounterEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
if req.Settings.JwtGroupsEnabled != nil {
|
||||
settings.JWTGroupsEnabled = *req.Settings.JwtGroupsEnabled
|
||||
}
|
||||
if req.Settings.GroupsPropagationEnabled != nil {
|
||||
settings.GroupsPropagationEnabled = *req.Settings.GroupsPropagationEnabled
|
||||
}
|
||||
if req.Settings.JwtGroupsClaimName != nil {
|
||||
settings.JWTGroupsClaimName = *req.Settings.JwtGroupsClaimName
|
||||
}
|
||||
if req.Settings.JwtAllowGroups != nil {
|
||||
settings.JWTAllowGroups = *req.Settings.JwtAllowGroups
|
||||
}
|
||||
if req.Settings.RoutingPeerDnsResolutionEnabled != nil {
|
||||
settings.RoutingPeerDNSResolutionEnabled = *req.Settings.RoutingPeerDnsResolutionEnabled
|
||||
}
|
||||
if req.Settings.DnsDomain != nil {
|
||||
settings.DNSDomain = *req.Settings.DnsDomain
|
||||
}
|
||||
if req.Settings.LazyConnectionEnabled != nil {
|
||||
settings.LazyConnectionEnabled = *req.Settings.LazyConnectionEnabled
|
||||
settings, err := h.updateAccountRequestSettings(req)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
}
|
||||
if req.Settings.NetworkRange != nil && *req.Settings.NetworkRange != "" {
|
||||
prefix, err := netip.ParsePrefix(*req.Settings.NetworkRange)
|
||||
@@ -312,6 +333,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
|
||||
RoutingPeerDnsResolutionEnabled: &settings.RoutingPeerDNSResolutionEnabled,
|
||||
LazyConnectionEnabled: &settings.LazyConnectionEnabled,
|
||||
DnsDomain: &settings.DNSDomain,
|
||||
AutoUpdateVersion: &settings.AutoUpdateVersion,
|
||||
}
|
||||
|
||||
if settings.NetworkRange.IsValid() {
|
||||
|
||||
@@ -120,6 +120,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
AutoUpdateVersion: sr(""),
|
||||
},
|
||||
expectedArray: true,
|
||||
expectedID: accountID,
|
||||
@@ -142,6 +143,30 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
AutoUpdateVersion: sr(""),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
},
|
||||
{
|
||||
name: "PutAccount OK with autoUpdateVersion",
|
||||
expectedBody: true,
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/accounts/" + accountID,
|
||||
requestBody: bytes.NewBufferString("{\"settings\": {\"auto_update_version\": \"latest\", \"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": true},\"onboarding\": {\"onboarding_flow_pending\": true,\"signup_form_pending\": true}}"),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedSettings: api.AccountSettings{
|
||||
PeerLoginExpiration: 15552000,
|
||||
PeerLoginExpirationEnabled: true,
|
||||
GroupsPropagationEnabled: br(false),
|
||||
JwtGroupsClaimName: sr(""),
|
||||
JwtGroupsEnabled: br(false),
|
||||
JwtAllowGroups: &[]string{},
|
||||
RegularUsersViewBlocked: false,
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
AutoUpdateVersion: sr("latest"),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -164,6 +189,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
AutoUpdateVersion: sr(""),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -186,6 +212,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
AutoUpdateVersion: sr(""),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -208,6 +235,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
AutoUpdateVersion: sr(""),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
|
||||
@@ -1552,6 +1552,7 @@ func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction sto
|
||||
FirewallRules: []*proto.FirewallRule{},
|
||||
FirewallRulesIsEmpty: true,
|
||||
},
|
||||
AutoUpdateVersion: settings.AutoUpdateVersion,
|
||||
},
|
||||
NetworkMap: &types.NetworkMap{},
|
||||
})
|
||||
|
||||
@@ -210,6 +210,7 @@ func (m *TimeBasedAuthSecretsManager) pushNewTURNAndRelayTokens(ctx context.Cont
|
||||
NetbirdConfig: &proto.NetbirdConfig{
|
||||
Turns: turns,
|
||||
},
|
||||
AutoUpdateVersion: "skip",
|
||||
}
|
||||
|
||||
// workaround for the case when client is unable to handle turn and relay updates at different time
|
||||
@@ -246,6 +247,7 @@ func (m *TimeBasedAuthSecretsManager) pushNewRelayTokens(ctx context.Context, ac
|
||||
},
|
||||
// omit Turns to avoid updates there
|
||||
},
|
||||
AutoUpdateVersion: "skip",
|
||||
}
|
||||
|
||||
m.extendNetbirdConfig(ctx, peerID, accountID, update)
|
||||
|
||||
@@ -52,6 +52,9 @@ type Settings struct {
|
||||
|
||||
// LazyConnectionEnabled indicates if the experimental feature is enabled or disabled
|
||||
LazyConnectionEnabled bool `gorm:"default:false"`
|
||||
|
||||
// AutoUpdateVersion client auto-update version
|
||||
AutoUpdateVersion string
|
||||
}
|
||||
|
||||
// Copy copies the Settings struct
|
||||
@@ -72,6 +75,7 @@ func (s *Settings) Copy() *Settings {
|
||||
LazyConnectionEnabled: s.LazyConnectionEnabled,
|
||||
DNSDomain: s.DNSDomain,
|
||||
NetworkRange: s.NetworkRange,
|
||||
AutoUpdateVersion: s.AutoUpdateVersion,
|
||||
}
|
||||
if s.Extra != nil {
|
||||
settings.Extra = s.Extra.Copy()
|
||||
|
||||
@@ -145,6 +145,10 @@ components:
|
||||
description: Enables or disables experimental lazy connection
|
||||
type: boolean
|
||||
example: true
|
||||
auto_update_version:
|
||||
description: Set Clients auto-update version. "latest", "disabled", or a specific version (e.g "0.50.1")
|
||||
type: string
|
||||
example: "0.51.2"
|
||||
required:
|
||||
- peer_login_expiration_enabled
|
||||
- peer_login_expiration
|
||||
|
||||
@@ -287,6 +287,9 @@ type AccountRequest struct {
|
||||
|
||||
// AccountSettings defines model for AccountSettings.
|
||||
type AccountSettings struct {
|
||||
// AutoUpdateVersion Set Clients auto-update version. "latest", "disabled", or a specific version (e.g "0.50.1")
|
||||
AutoUpdateVersion *string `json:"auto_update_version,omitempty"`
|
||||
|
||||
// DnsDomain Allows to define a custom dns domain for the account
|
||||
DnsDomain *string `json:"dns_domain,omitempty"`
|
||||
Extra *AccountExtraSettings `json:"extra,omitempty"`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,9 @@ message SyncResponse {
|
||||
|
||||
// Posture checks to be evaluated by client
|
||||
repeated Checks Checks = 6;
|
||||
|
||||
// Auto-update config
|
||||
string autoUpdateVersion = 7;
|
||||
}
|
||||
|
||||
message SyncMetaRequest {
|
||||
|
||||
@@ -30,7 +30,7 @@ type Update struct {
|
||||
fetchTicker *time.Ticker
|
||||
fetchDone chan struct{}
|
||||
|
||||
onUpdateListener func()
|
||||
onUpdateListener func(latestVersion string)
|
||||
listenerLock sync.Mutex
|
||||
}
|
||||
|
||||
@@ -57,11 +57,7 @@ func NewUpdate(httpAgent string) *Update {
|
||||
// StopWatch stop the version info fetch loop
|
||||
func (u *Update) StopWatch() {
|
||||
u.fetchTicker.Stop()
|
||||
|
||||
select {
|
||||
case u.fetchDone <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
u.fetchDone <- struct{}{}
|
||||
}
|
||||
|
||||
// SetDaemonVersion update the currently running daemon version. If new version is available it will trigger
|
||||
@@ -84,13 +80,15 @@ func (u *Update) SetDaemonVersion(newVersion string) bool {
|
||||
}
|
||||
|
||||
// SetOnUpdateListener set new update listener
|
||||
func (u *Update) SetOnUpdateListener(updateFn func()) {
|
||||
func (u *Update) SetOnUpdateListener(updateFn func(string)) {
|
||||
u.listenerLock.Lock()
|
||||
defer u.listenerLock.Unlock()
|
||||
|
||||
u.onUpdateListener = updateFn
|
||||
if u.isUpdateAvailable() {
|
||||
u.onUpdateListener()
|
||||
u.versionsLock.Lock()
|
||||
defer u.versionsLock.Unlock()
|
||||
u.onUpdateListener(u.latestAvailable.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +171,7 @@ func (u *Update) checkUpdate() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
go u.onUpdateListener()
|
||||
go u.onUpdateListener(u.latestAvailable.String())
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestNewUpdate(t *testing.T) {
|
||||
onUpdate := false
|
||||
u := NewUpdate(httpAgent)
|
||||
defer u.StopWatch()
|
||||
u.SetOnUpdateListener(func() {
|
||||
u.SetOnUpdateListener(func(_ string) {
|
||||
onUpdate = true
|
||||
wg.Done()
|
||||
})
|
||||
@@ -50,7 +50,7 @@ func TestDoNotUpdate(t *testing.T) {
|
||||
onUpdate := false
|
||||
u := NewUpdate(httpAgent)
|
||||
defer u.StopWatch()
|
||||
u.SetOnUpdateListener(func() {
|
||||
u.SetOnUpdateListener(func(_ string) {
|
||||
onUpdate = true
|
||||
wg.Done()
|
||||
})
|
||||
@@ -75,7 +75,7 @@ func TestDaemonUpdate(t *testing.T) {
|
||||
onUpdate := false
|
||||
u := NewUpdate(httpAgent)
|
||||
defer u.StopWatch()
|
||||
u.SetOnUpdateListener(func() {
|
||||
u.SetOnUpdateListener(func(_ string) {
|
||||
onUpdate = true
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user