[client] Feat: Support Multiple Profiles (#3980)

[client] Feat: Support Multiple Profiles (#3980)
This commit is contained in:
hakansa
2025-07-25 16:54:46 +03:00
committed by GitHub
parent e0d9306b05
commit cb8b6ca59b
53 changed files with 4651 additions and 768 deletions

View File

@ -1,3 +1,6 @@
//go:build windows
// +build windows
package server
import (

View File

@ -22,6 +22,7 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/netbirdio/netbird/client/internal/auth"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/management/domain"
@ -50,14 +51,12 @@ type Server struct {
rootCtx context.Context
actCancel context.CancelFunc
latestConfigInput internal.ConfigInput
logFile string
oauthAuthFlow oauthAuthFlow
mutex sync.Mutex
config *internal.Config
config *profilemanager.Config
proto.UnimplementedDaemonServiceServer
connectClient *internal.ConnectClient
@ -68,6 +67,8 @@ type Server struct {
lastProbe time.Time
persistNetworkMap bool
isSessionActive atomic.Bool
profileManager profilemanager.ServiceManager
}
type oauthAuthFlow struct {
@ -78,15 +79,13 @@ type oauthAuthFlow struct {
}
// New server instance constructor.
func New(ctx context.Context, configPath, logFile string) *Server {
func New(ctx context.Context, logFile string) *Server {
return &Server{
rootCtx: ctx,
latestConfigInput: internal.ConfigInput{
ConfigPath: configPath,
},
rootCtx: ctx,
logFile: logFile,
persistNetworkMap: true,
statusRecorder: peer.NewRecorder(""),
profileManager: profilemanager.ServiceManager{},
}
}
@ -99,7 +98,7 @@ func (s *Server) Start() error {
log.Warnf("failed to redirect stderr: %v", err)
}
if err := restoreResidualState(s.rootCtx); err != nil {
if err := restoreResidualState(s.rootCtx, s.profileManager.GetStatePath()); err != nil {
log.Warnf(errRestoreResidualState, err)
}
@ -118,25 +117,41 @@ func (s *Server) Start() error {
ctx, cancel := context.WithCancel(s.rootCtx)
s.actCancel = cancel
// if configuration exists, we just start connections. if is new config we skip and set status NeedsLogin
// on failure we return error to retry
config, err := internal.UpdateConfig(s.latestConfigInput)
if errorStatus, ok := gstatus.FromError(err); ok && errorStatus.Code() == codes.NotFound {
s.config, err = internal.UpdateOrCreateConfig(s.latestConfigInput)
if err != nil {
log.Warnf("unable to create configuration file: %v", err)
return err
}
state.Set(internal.StatusNeedsLogin)
return nil
} else if err != nil {
log.Warnf("unable to create configuration file: %v", err)
return err
// set the default config if not exists
if err := s.setDefaultConfigIfNotExists(ctx); err != nil {
log.Errorf("failed to set default config: %v", err)
return fmt.Errorf("failed to set default config: %w", err)
}
// if configuration exists, we just start connections.
config, _ = internal.UpdateOldManagementURL(ctx, config, s.latestConfigInput.ConfigPath)
activeProf, err := s.profileManager.GetActiveProfileState()
if err != nil {
return fmt.Errorf("failed to get active profile state: %w", err)
}
cfgPath, err := activeProf.FilePath()
if err != nil {
log.Errorf("failed to get active profile file path: %v", err)
return fmt.Errorf("failed to get active profile file path: %w", err)
}
config, err := profilemanager.GetConfig(cfgPath)
if err != nil {
log.Errorf("failed to get active profile config: %v", err)
if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{
Name: "default",
Username: "",
}); err != nil {
log.Errorf("failed to set active profile state: %v", err)
return fmt.Errorf("failed to set active profile state: %w", err)
}
config, err = profilemanager.GetConfig(s.profileManager.DefaultProfilePath())
if err != nil {
log.Errorf("failed to get default profile config: %v", err)
return fmt.Errorf("failed to get default profile config: %w", err)
}
}
s.config = config
s.statusRecorder.UpdateManagementAddress(config.ManagementURL.String())
@ -157,10 +172,34 @@ func (s *Server) Start() error {
return nil
}
func (s *Server) setDefaultConfigIfNotExists(ctx context.Context) error {
ok, err := s.profileManager.CopyDefaultProfileIfNotExists()
if err != nil {
if err := s.profileManager.CreateDefaultProfile(); err != nil {
log.Errorf("failed to create default profile: %v", err)
return fmt.Errorf("failed to create default profile: %w", err)
}
if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{
Name: "default",
Username: "",
}); err != nil {
log.Errorf("failed to set active profile state: %v", err)
return fmt.Errorf("failed to set active profile state: %w", err)
}
}
if ok {
state := internal.CtxGetState(ctx)
state.Set(internal.StatusNeedsLogin)
}
return nil
}
// 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.
// 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, config *internal.Config, statusRecorder *peer.Status,
func (s *Server) connectWithRetryRuns(ctx context.Context, config *profilemanager.Config, statusRecorder *peer.Status,
runningChan chan struct{},
) {
backOff := getConnectWithBackoff(ctx)
@ -276,6 +315,90 @@ func (s *Server) loginAttempt(ctx context.Context, setupKey, jwtToken string) (i
return "", nil
}
// Login uses setup key to prepare configuration for the daemon.
func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigRequest) (*proto.SetConfigResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
profState := profilemanager.ActiveProfileState{
Name: msg.ProfileName,
Username: msg.Username,
}
profPath, err := profState.FilePath()
if err != nil {
log.Errorf("failed to get active profile file path: %v", err)
return nil, fmt.Errorf("failed to get active profile file path: %w", err)
}
var config profilemanager.ConfigInput
config.ConfigPath = profPath
if msg.ManagementUrl != "" {
config.ManagementURL = msg.ManagementUrl
}
if msg.AdminURL != "" {
config.AdminURL = msg.AdminURL
}
if msg.InterfaceName != nil {
config.InterfaceName = msg.InterfaceName
}
if msg.WireguardPort != nil {
wgPort := int(*msg.WireguardPort)
config.WireguardPort = &wgPort
}
if msg.OptionalPreSharedKey != nil {
if *msg.OptionalPreSharedKey != "" {
config.PreSharedKey = msg.OptionalPreSharedKey
}
}
if msg.CleanDNSLabels {
config.DNSLabels = domain.List{}
} else if msg.DnsLabels != nil {
dnsLabels := domain.FromPunycodeList(msg.DnsLabels)
config.DNSLabels = dnsLabels
}
if msg.CleanNATExternalIPs {
config.NATExternalIPs = make([]string, 0)
} else if msg.NatExternalIPs != nil {
config.NATExternalIPs = msg.NatExternalIPs
}
config.CustomDNSAddress = msg.CustomDNSAddress
if string(msg.CustomDNSAddress) == "empty" {
config.CustomDNSAddress = []byte{}
}
config.RosenpassEnabled = msg.RosenpassEnabled
config.RosenpassPermissive = msg.RosenpassPermissive
config.DisableAutoConnect = msg.DisableAutoConnect
config.ServerSSHAllowed = msg.ServerSSHAllowed
config.NetworkMonitor = msg.NetworkMonitor
config.DisableClientRoutes = msg.DisableClientRoutes
config.DisableServerRoutes = msg.DisableServerRoutes
config.DisableDNS = msg.DisableDns
config.DisableFirewall = msg.DisableFirewall
config.BlockLANAccess = msg.BlockLanAccess
config.DisableNotifications = msg.DisableNotifications
config.LazyConnectionEnabled = msg.LazyConnectionEnabled
config.BlockInbound = msg.BlockInbound
if _, err := profilemanager.UpdateConfig(config); err != nil {
log.Errorf("failed to update profile config: %v", err)
return nil, fmt.Errorf("failed to update profile config: %w", err)
}
return &proto.SetConfigResponse{}, nil
}
// Login uses setup key to prepare configuration for the daemon.
func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*proto.LoginResponse, error) {
s.mutex.Lock()
@ -292,7 +415,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
s.actCancel = cancel
s.mutex.Unlock()
if err := restoreResidualState(ctx); err != nil {
if err := restoreResidualState(ctx, s.profileManager.GetStatePath()); err != nil {
log.Warnf(errRestoreResidualState, err)
}
@ -304,147 +427,62 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
}
}()
activeProf, err := s.profileManager.GetActiveProfileState()
if err != nil {
log.Errorf("failed to get active profile state: %v", err)
return nil, fmt.Errorf("failed to get active profile state: %w", err)
}
if msg.ProfileName != nil {
if *msg.ProfileName != "default" && (msg.Username == nil || *msg.Username == "") {
log.Errorf("profile name is set to %s, but username is not provided", *msg.ProfileName)
return nil, fmt.Errorf("profile name is set to %s, but username is not provided", *msg.ProfileName)
}
var username string
if *msg.ProfileName != "default" {
username = *msg.Username
}
if *msg.ProfileName != activeProf.Name && username != activeProf.Username {
log.Infof("switching to profile %s for user '%s'", *msg.ProfileName, username)
if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{
Name: *msg.ProfileName,
Username: username,
}); err != nil {
log.Errorf("failed to set active profile state: %v", err)
return nil, fmt.Errorf("failed to set active profile state: %w", err)
}
}
}
activeProf, err = s.profileManager.GetActiveProfileState()
if err != nil {
log.Errorf("failed to get active profile state: %v", err)
return nil, fmt.Errorf("failed to get active profile state: %w", err)
}
log.Infof("active profile: %s for %s", activeProf.Name, activeProf.Username)
s.mutex.Lock()
inputConfig := s.latestConfigInput
if msg.ManagementUrl != "" {
inputConfig.ManagementURL = msg.ManagementUrl
s.latestConfigInput.ManagementURL = msg.ManagementUrl
}
if msg.AdminURL != "" {
inputConfig.AdminURL = msg.AdminURL
s.latestConfigInput.AdminURL = msg.AdminURL
}
if msg.CleanNATExternalIPs {
inputConfig.NATExternalIPs = make([]string, 0)
s.latestConfigInput.NATExternalIPs = nil
} else if msg.NatExternalIPs != nil {
inputConfig.NATExternalIPs = msg.NatExternalIPs
s.latestConfigInput.NATExternalIPs = msg.NatExternalIPs
}
inputConfig.CustomDNSAddress = msg.CustomDNSAddress
s.latestConfigInput.CustomDNSAddress = msg.CustomDNSAddress
if string(msg.CustomDNSAddress) == "empty" {
inputConfig.CustomDNSAddress = []byte{}
s.latestConfigInput.CustomDNSAddress = []byte{}
}
if msg.Hostname != "" {
// nolint
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, msg.Hostname)
}
if msg.RosenpassEnabled != nil {
inputConfig.RosenpassEnabled = msg.RosenpassEnabled
s.latestConfigInput.RosenpassEnabled = msg.RosenpassEnabled
}
if msg.RosenpassPermissive != nil {
inputConfig.RosenpassPermissive = msg.RosenpassPermissive
s.latestConfigInput.RosenpassPermissive = msg.RosenpassPermissive
}
if msg.ServerSSHAllowed != nil {
inputConfig.ServerSSHAllowed = msg.ServerSSHAllowed
s.latestConfigInput.ServerSSHAllowed = msg.ServerSSHAllowed
}
if msg.DisableAutoConnect != nil {
inputConfig.DisableAutoConnect = msg.DisableAutoConnect
s.latestConfigInput.DisableAutoConnect = msg.DisableAutoConnect
}
if msg.InterfaceName != nil {
inputConfig.InterfaceName = msg.InterfaceName
s.latestConfigInput.InterfaceName = msg.InterfaceName
}
if msg.WireguardPort != nil {
port := int(*msg.WireguardPort)
inputConfig.WireguardPort = &port
s.latestConfigInput.WireguardPort = &port
}
if msg.NetworkMonitor != nil {
inputConfig.NetworkMonitor = msg.NetworkMonitor
s.latestConfigInput.NetworkMonitor = msg.NetworkMonitor
}
if len(msg.ExtraIFaceBlacklist) > 0 {
inputConfig.ExtraIFaceBlackList = msg.ExtraIFaceBlacklist
s.latestConfigInput.ExtraIFaceBlackList = msg.ExtraIFaceBlacklist
}
if msg.DnsRouteInterval != nil {
duration := msg.DnsRouteInterval.AsDuration()
inputConfig.DNSRouteInterval = &duration
s.latestConfigInput.DNSRouteInterval = &duration
}
if msg.DisableClientRoutes != nil {
inputConfig.DisableClientRoutes = msg.DisableClientRoutes
s.latestConfigInput.DisableClientRoutes = msg.DisableClientRoutes
}
if msg.DisableServerRoutes != nil {
inputConfig.DisableServerRoutes = msg.DisableServerRoutes
s.latestConfigInput.DisableServerRoutes = msg.DisableServerRoutes
}
if msg.DisableDns != nil {
inputConfig.DisableDNS = msg.DisableDns
s.latestConfigInput.DisableDNS = msg.DisableDns
}
if msg.DisableFirewall != nil {
inputConfig.DisableFirewall = msg.DisableFirewall
s.latestConfigInput.DisableFirewall = msg.DisableFirewall
}
if msg.BlockLanAccess != nil {
inputConfig.BlockLANAccess = msg.BlockLanAccess
s.latestConfigInput.BlockLANAccess = msg.BlockLanAccess
}
if msg.BlockInbound != nil {
inputConfig.BlockInbound = msg.BlockInbound
s.latestConfigInput.BlockInbound = msg.BlockInbound
}
if msg.CleanDNSLabels {
inputConfig.DNSLabels = domain.List{}
s.latestConfigInput.DNSLabels = nil
} else if msg.DnsLabels != nil {
dnsLabels := domain.FromPunycodeList(msg.DnsLabels)
inputConfig.DNSLabels = dnsLabels
s.latestConfigInput.DNSLabels = dnsLabels
}
if msg.DisableNotifications != nil {
inputConfig.DisableNotifications = msg.DisableNotifications
s.latestConfigInput.DisableNotifications = msg.DisableNotifications
}
if msg.LazyConnectionEnabled != nil {
inputConfig.LazyConnectionEnabled = msg.LazyConnectionEnabled
s.latestConfigInput.LazyConnectionEnabled = msg.LazyConnectionEnabled
}
s.mutex.Unlock()
if msg.OptionalPreSharedKey != nil {
inputConfig.PreSharedKey = msg.OptionalPreSharedKey
}
config, err := internal.UpdateOrCreateConfig(inputConfig)
cfgPath, err := activeProf.FilePath()
if err != nil {
return nil, err
log.Errorf("failed to get active profile file path: %v", err)
return nil, fmt.Errorf("failed to get active profile file path: %w", err)
}
if msg.ManagementUrl == "" {
config, _ = internal.UpdateOldManagementURL(ctx, config, s.latestConfigInput.ConfigPath)
s.config = config
s.latestConfigInput.ManagementURL = config.ManagementURL.String()
config, err := profilemanager.GetConfig(cfgPath)
if err != nil {
log.Errorf("failed to get active profile config: %v", err)
return nil, fmt.Errorf("failed to get active profile config: %w", err)
}
s.mutex.Lock()
s.config = config
s.mutex.Unlock()
@ -586,15 +624,17 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
return nil, err
}
return &proto.WaitSSOLoginResponse{}, nil
return &proto.WaitSSOLoginResponse{
Email: tokenInfo.Email,
}, nil
}
// Up starts engine work in the daemon.
func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpResponse, error) {
func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if err := restoreResidualState(callerCtx); err != nil {
if err := restoreResidualState(callerCtx, s.profileManager.GetStatePath()); err != nil {
log.Warnf(errRestoreResidualState, err)
}
@ -628,6 +668,40 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes
return nil, fmt.Errorf("config is not defined, please call login command first")
}
activeProf, err := s.profileManager.GetActiveProfileState()
if err != nil {
log.Errorf("failed to get active profile state: %v", err)
return nil, fmt.Errorf("failed to get active profile state: %w", err)
}
if msg != nil && msg.ProfileName != nil {
if err := s.switchProfileIfNeeded(*msg.ProfileName, msg.Username, activeProf); err != nil {
log.Errorf("failed to switch profile: %v", err)
return nil, fmt.Errorf("failed to switch profile: %w", err)
}
}
activeProf, err = s.profileManager.GetActiveProfileState()
if err != nil {
log.Errorf("failed to get active profile state: %v", err)
return nil, fmt.Errorf("failed to get active profile state: %w", err)
}
log.Infof("active profile: %s for %s", activeProf.Name, activeProf.Username)
cfgPath, err := activeProf.FilePath()
if err != nil {
log.Errorf("failed to get active profile file path: %v", err)
return nil, fmt.Errorf("failed to get active profile file path: %w", err)
}
config, err := profilemanager.GetConfig(cfgPath)
if err != nil {
log.Errorf("failed to get active profile config: %v", err)
return nil, fmt.Errorf("failed to get active profile config: %w", err)
}
s.config = config
s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String())
s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive)
@ -651,6 +725,70 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes
}
}
func (s *Server) switchProfileIfNeeded(profileName string, userName *string, activeProf *profilemanager.ActiveProfileState) error {
if profileName != "default" && (userName == nil || *userName == "") {
log.Errorf("profile name is set to %s, but username is not provided", profileName)
return fmt.Errorf("profile name is set to %s, but username is not provided", profileName)
}
var username string
if profileName != "default" {
username = *userName
}
if profileName != activeProf.Name || username != activeProf.Username {
log.Infof("switching to profile %s for user %s", profileName, username)
if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{
Name: profileName,
Username: username,
}); err != nil {
log.Errorf("failed to set active profile state: %v", err)
return fmt.Errorf("failed to set active profile state: %w", err)
}
}
return nil
}
// SwitchProfile switches the active profile in the daemon.
func (s *Server) SwitchProfile(callerCtx context.Context, msg *proto.SwitchProfileRequest) (*proto.SwitchProfileResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
activeProf, err := s.profileManager.GetActiveProfileState()
if err != nil {
log.Errorf("failed to get active profile state: %v", err)
return nil, fmt.Errorf("failed to get active profile state: %w", err)
}
if msg != nil && msg.ProfileName != nil {
if err := s.switchProfileIfNeeded(*msg.ProfileName, msg.Username, activeProf); err != nil {
log.Errorf("failed to switch profile: %v", err)
return nil, fmt.Errorf("failed to switch profile: %w", err)
}
}
activeProf, err = s.profileManager.GetActiveProfileState()
if err != nil {
log.Errorf("failed to get active profile state: %v", err)
return nil, fmt.Errorf("failed to get active profile state: %w", err)
}
cfgPath, err := activeProf.FilePath()
if err != nil {
log.Errorf("failed to get active profile file path: %v", err)
return nil, fmt.Errorf("failed to get active profile file path: %w", err)
}
config, err := profilemanager.GetConfig(cfgPath)
if err != nil {
log.Errorf("failed to get default profile config: %v", err)
return nil, fmt.Errorf("failed to get default profile config: %w", err)
}
s.config = config
return &proto.SwitchProfileResponse{}, nil
}
// Down engine work in the daemon.
func (s *Server) Down(ctx context.Context, _ *proto.DownRequest) (*proto.DownResponse, error) {
s.mutex.Lock()
@ -738,58 +876,65 @@ func (s *Server) runProbes() {
}
// GetConfig of the daemon.
func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto.GetConfigResponse, error) {
func (s *Server) GetConfig(ctx context.Context, req *proto.GetConfigRequest) (*proto.GetConfigResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
managementURL := s.latestConfigInput.ManagementURL
adminURL := s.latestConfigInput.AdminURL
preSharedKey := ""
if ctx.Err() != nil {
return nil, ctx.Err()
}
if s.config != nil {
if managementURL == "" && s.config.ManagementURL != nil {
managementURL = s.config.ManagementURL.String()
}
prof := profilemanager.ActiveProfileState{
Name: req.ProfileName,
Username: req.Username,
}
if s.config.AdminURL != nil {
adminURL = s.config.AdminURL.String()
}
cfgPath, err := prof.FilePath()
if err != nil {
log.Errorf("failed to get active profile file path: %v", err)
return nil, fmt.Errorf("failed to get active profile file path: %w", err)
}
preSharedKey = s.config.PreSharedKey
if preSharedKey != "" {
preSharedKey = "**********"
}
cfg, err := profilemanager.GetConfig(cfgPath)
if err != nil {
log.Errorf("failed to get active profile config: %v", err)
return nil, fmt.Errorf("failed to get active profile config: %w", err)
}
managementURL := cfg.ManagementURL
adminURL := cfg.AdminURL
var preSharedKey = cfg.PreSharedKey
if preSharedKey != "" {
preSharedKey = "**********"
}
disableNotifications := true
if s.config.DisableNotifications != nil {
disableNotifications = *s.config.DisableNotifications
if cfg.DisableNotifications != nil {
disableNotifications = *cfg.DisableNotifications
}
networkMonitor := false
if s.config.NetworkMonitor != nil {
networkMonitor = *s.config.NetworkMonitor
if cfg.NetworkMonitor != nil {
networkMonitor = *cfg.NetworkMonitor
}
disableDNS := s.config.DisableDNS
disableClientRoutes := s.config.DisableClientRoutes
disableServerRoutes := s.config.DisableServerRoutes
blockLANAccess := s.config.BlockLANAccess
disableDNS := cfg.DisableDNS
disableClientRoutes := cfg.DisableClientRoutes
disableServerRoutes := cfg.DisableServerRoutes
blockLANAccess := cfg.BlockLANAccess
return &proto.GetConfigResponse{
ManagementUrl: managementURL,
ConfigFile: s.latestConfigInput.ConfigPath,
LogFile: s.logFile,
ManagementUrl: managementURL.String(),
PreSharedKey: preSharedKey,
AdminURL: adminURL,
InterfaceName: s.config.WgIface,
WireguardPort: int64(s.config.WgPort),
DisableAutoConnect: s.config.DisableAutoConnect,
ServerSSHAllowed: *s.config.ServerSSHAllowed,
RosenpassEnabled: s.config.RosenpassEnabled,
RosenpassPermissive: s.config.RosenpassPermissive,
LazyConnectionEnabled: s.config.LazyConnectionEnabled,
BlockInbound: s.config.BlockInbound,
AdminURL: adminURL.String(),
InterfaceName: cfg.WgIface,
WireguardPort: int64(cfg.WgPort),
DisableAutoConnect: cfg.DisableAutoConnect,
ServerSSHAllowed: *cfg.ServerSSHAllowed,
RosenpassEnabled: cfg.RosenpassEnabled,
RosenpassPermissive: cfg.RosenpassPermissive,
LazyConnectionEnabled: cfg.LazyConnectionEnabled,
BlockInbound: cfg.BlockInbound,
DisableNotifications: disableNotifications,
NetworkMonitor: networkMonitor,
DisableDns: disableDNS,
@ -918,3 +1063,82 @@ func sendTerminalNotification() error {
return wallCmd.Wait()
}
// AddProfile adds a new profile to the daemon.
func (s *Server) AddProfile(ctx context.Context, msg *proto.AddProfileRequest) (*proto.AddProfileResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if msg.ProfileName == "" || msg.Username == "" {
return nil, gstatus.Errorf(codes.InvalidArgument, "profile name and username must be provided")
}
if err := s.profileManager.AddProfile(msg.ProfileName, msg.Username); err != nil {
log.Errorf("failed to create profile: %v", err)
return nil, fmt.Errorf("failed to create profile: %w", err)
}
return &proto.AddProfileResponse{}, nil
}
// RemoveProfile removes a profile from the daemon.
func (s *Server) RemoveProfile(ctx context.Context, msg *proto.RemoveProfileRequest) (*proto.RemoveProfileResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if msg.ProfileName == "" {
return nil, gstatus.Errorf(codes.InvalidArgument, "profile name must be provided")
}
if err := s.profileManager.RemoveProfile(msg.ProfileName, msg.Username); err != nil {
log.Errorf("failed to remove profile: %v", err)
return nil, fmt.Errorf("failed to remove profile: %w", err)
}
return &proto.RemoveProfileResponse{}, nil
}
// ListProfiles lists all profiles in the daemon.
func (s *Server) ListProfiles(ctx context.Context, msg *proto.ListProfilesRequest) (*proto.ListProfilesResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if msg.Username == "" {
return nil, gstatus.Errorf(codes.InvalidArgument, "username must be provided")
}
profiles, err := s.profileManager.ListProfiles(msg.Username)
if err != nil {
log.Errorf("failed to list profiles: %v", err)
return nil, fmt.Errorf("failed to list profiles: %w", err)
}
response := &proto.ListProfilesResponse{
Profiles: make([]*proto.Profile, len(profiles)),
}
for i, profile := range profiles {
response.Profiles[i] = &proto.Profile{
Name: profile.Name,
IsActive: profile.IsActive,
}
}
return response, nil
}
// GetActiveProfile returns the active profile in the daemon.
func (s *Server) GetActiveProfile(ctx context.Context, msg *proto.GetActiveProfileRequest) (*proto.GetActiveProfileResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
activeProfile, err := s.profileManager.GetActiveProfileState()
if err != nil {
log.Errorf("failed to get active profile state: %v", err)
return nil, fmt.Errorf("failed to get active profile state: %w", err)
}
return &proto.GetActiveProfileResponse{
ProfileName: activeProfile.Name,
Username: activeProfile.Username,
}, nil
}

View File

@ -4,6 +4,8 @@ import (
"context"
"net"
"net/url"
"os/user"
"path/filepath"
"testing"
"time"
@ -20,6 +22,7 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
daemonProto "github.com/netbirdio/netbird/client/proto"
mgmtProto "github.com/netbirdio/netbird/management/proto"
"github.com/netbirdio/netbird/management/server"
@ -32,7 +35,6 @@ import (
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/signal/proto"
signalServer "github.com/netbirdio/netbird/signal/server"
"github.com/netbirdio/netbird/util"
)
var (
@ -70,12 +72,30 @@ func TestConnectWithRetryRuns(t *testing.T) {
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(30*time.Second))
defer cancel()
// create new server
s := New(ctx, t.TempDir()+"/config.json", "debug")
s.latestConfigInput.ManagementURL = "http://" + mgmtAddr
config, err := internal.UpdateOrCreateConfig(s.latestConfigInput)
ic := profilemanager.ConfigInput{
ManagementURL: "http://" + mgmtAddr,
ConfigPath: t.TempDir() + "/test-profile.json",
}
config, err := profilemanager.UpdateOrCreateConfig(ic)
if err != nil {
t.Fatalf("failed to create config: %v", err)
}
currUser, err := user.Current()
require.NoError(t, err)
pm := profilemanager.ServiceManager{}
err = pm.SetActiveProfileState(&profilemanager.ActiveProfileState{
Name: "test-profile",
Username: currUser.Username,
})
if err != nil {
t.Fatalf("failed to set active profile state: %v", err)
}
s := New(ctx, "debug")
s.config = config
s.statusRecorder = peer.NewRecorder(config.ManagementURL.String())
@ -91,26 +111,67 @@ func TestConnectWithRetryRuns(t *testing.T) {
}
func TestServer_Up(t *testing.T) {
tempDir := t.TempDir()
origDefaultProfileDir := profilemanager.DefaultConfigPathDir
origDefaultConfigPath := profilemanager.DefaultConfigPath
profilemanager.ConfigDirOverride = tempDir
origActiveProfileStatePath := profilemanager.ActiveProfileStatePath
profilemanager.DefaultConfigPathDir = tempDir
profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json"
profilemanager.DefaultConfigPath = filepath.Join(tempDir, "default.json")
t.Cleanup(func() {
profilemanager.DefaultConfigPathDir = origDefaultProfileDir
profilemanager.ActiveProfileStatePath = origActiveProfileStatePath
profilemanager.DefaultConfigPath = origDefaultConfigPath
profilemanager.ConfigDirOverride = ""
})
ctx := internal.CtxInitState(context.Background())
s := New(ctx, t.TempDir()+"/config.json", util.LogConsole)
currUser, err := user.Current()
require.NoError(t, err)
err := s.Start()
profName := "default"
ic := profilemanager.ConfigInput{
ConfigPath: filepath.Join(tempDir, profName+".json"),
}
_, err = profilemanager.UpdateOrCreateConfig(ic)
if err != nil {
t.Fatalf("failed to create config: %v", err)
}
pm := profilemanager.ServiceManager{}
err = pm.SetActiveProfileState(&profilemanager.ActiveProfileState{
Name: profName,
Username: currUser.Username,
})
if err != nil {
t.Fatalf("failed to set active profile state: %v", err)
}
s := New(ctx, "console")
err = s.Start()
require.NoError(t, err)
u, err := url.Parse("http://non-existent-url-for-testing.invalid:12345")
require.NoError(t, err)
s.config = &internal.Config{
s.config = &profilemanager.Config{
ManagementURL: u,
}
upCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
upReq := &daemonProto.UpRequest{}
upReq := &daemonProto.UpRequest{
ProfileName: &profName,
Username: &currUser.Username,
}
_, err = s.Up(upCtx, upReq)
assert.Contains(t, err.Error(), "NeedsLogin")
assert.Contains(t, err.Error(), "context deadline exceeded")
}
type mockSubscribeEventsServer struct {
@ -129,16 +190,51 @@ func (m *mockSubscribeEventsServer) Context() context.Context {
}
func TestServer_SubcribeEvents(t *testing.T) {
tempDir := t.TempDir()
origDefaultProfileDir := profilemanager.DefaultConfigPathDir
origDefaultConfigPath := profilemanager.DefaultConfigPath
profilemanager.ConfigDirOverride = tempDir
origActiveProfileStatePath := profilemanager.ActiveProfileStatePath
profilemanager.DefaultConfigPathDir = tempDir
profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json"
profilemanager.DefaultConfigPath = filepath.Join(tempDir, "default.json")
t.Cleanup(func() {
profilemanager.DefaultConfigPathDir = origDefaultProfileDir
profilemanager.ActiveProfileStatePath = origActiveProfileStatePath
profilemanager.DefaultConfigPath = origDefaultConfigPath
profilemanager.ConfigDirOverride = ""
})
ctx := internal.CtxInitState(context.Background())
ic := profilemanager.ConfigInput{
ConfigPath: tempDir + "/default.json",
}
s := New(ctx, t.TempDir()+"/config.json", util.LogConsole)
_, err := profilemanager.UpdateOrCreateConfig(ic)
if err != nil {
t.Fatalf("failed to create config: %v", err)
}
err := s.Start()
currUser, err := user.Current()
require.NoError(t, err)
pm := profilemanager.ServiceManager{}
err = pm.SetActiveProfileState(&profilemanager.ActiveProfileState{
Name: "default",
Username: currUser.Username,
})
if err != nil {
t.Fatalf("failed to set active profile state: %v", err)
}
s := New(ctx, "console")
err = s.Start()
require.NoError(t, err)
u, err := url.Parse("http://non-existent-url-for-testing.invalid:12345")
require.NoError(t, err)
s.config = &internal.Config{
s.config = &profilemanager.Config{
ManagementURL: u,
}

View File

@ -16,7 +16,7 @@ import (
// ListStates returns a list of all saved states
func (s *Server) ListStates(_ context.Context, _ *proto.ListStatesRequest) (*proto.ListStatesResponse, error) {
mgr := statemanager.New(statemanager.GetDefaultStatePath())
mgr := statemanager.New(s.profileManager.GetStatePath())
stateNames, err := mgr.GetSavedStateNames()
if err != nil {
@ -41,14 +41,16 @@ func (s *Server) CleanState(ctx context.Context, req *proto.CleanStateRequest) (
return nil, status.Errorf(codes.FailedPrecondition, "cannot clean state while connecting or connected, run 'netbird down' first.")
}
statePath := s.profileManager.GetStatePath()
if req.All {
// Reuse existing cleanup logic for all states
if err := restoreResidualState(ctx); err != nil {
if err := restoreResidualState(ctx, statePath); err != nil {
return nil, status.Errorf(codes.Internal, "failed to clean all states: %v", err)
}
// Get count of cleaned states
mgr := statemanager.New(statemanager.GetDefaultStatePath())
mgr := statemanager.New(statePath)
stateNames, err := mgr.GetSavedStateNames()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get state count: %v", err)
@ -60,7 +62,7 @@ func (s *Server) CleanState(ctx context.Context, req *proto.CleanStateRequest) (
}
// Handle single state cleanup
mgr := statemanager.New(statemanager.GetDefaultStatePath())
mgr := statemanager.New(statePath)
registerStates(mgr)
if err := mgr.CleanupStateByName(req.StateName); err != nil {
@ -82,7 +84,7 @@ func (s *Server) DeleteState(ctx context.Context, req *proto.DeleteStateRequest)
return nil, status.Errorf(codes.FailedPrecondition, "cannot clean state while connecting or connected, run 'netbird down' first.")
}
mgr := statemanager.New(statemanager.GetDefaultStatePath())
mgr := statemanager.New(s.profileManager.GetStatePath())
var count int
var err error
@ -112,13 +114,12 @@ func (s *Server) DeleteState(ctx context.Context, req *proto.DeleteStateRequest)
// restoreResidualState checks if the client was not shut down in a clean way and restores residual if required.
// Otherwise, we might not be able to connect to the management server to retrieve new config.
func restoreResidualState(ctx context.Context) error {
path := statemanager.GetDefaultStatePath()
if path == "" {
func restoreResidualState(ctx context.Context, statePath string) error {
if statePath == "" {
return nil
}
mgr := statemanager.New(path)
mgr := statemanager.New(statePath)
// register the states we are interested in restoring
registerStates(mgr)