diff --git a/client/cmd/root.go b/client/cmd/root.go index 0f9330601..8aa0d7c89 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -73,6 +73,7 @@ var ( dnsRouteInterval time.Duration lazyConnEnabled bool profilesDisabled bool + updateSettingsDisabled bool rootCmd = &cobra.Command{ Use: "netbird", diff --git a/client/cmd/service.go b/client/cmd/service.go index 997520f4c..b0431a69b 100644 --- a/client/cmd/service.go +++ b/client/cmd/service.go @@ -42,7 +42,8 @@ func init() { } serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd) - serviceCmd.PersistentFlags().BoolVar(&profilesDisabled, "disable-profiles", false, "Disables profiles feature. If enabled, the client will not be able to change or edit any profile.") + serviceCmd.PersistentFlags().BoolVar(&profilesDisabled, "disable-profiles", false, "Disables profiles feature. If enabled, the client will not be able to change or edit any profile. To persist this setting, use: netbird service install --disable-profiles") + serviceCmd.PersistentFlags().BoolVar(&updateSettingsDisabled, "disable-update-settings", false, "Disables update settings feature. If enabled, the client will not be able to change or edit any settings. To persist this setting, use: netbird service install --disable-update-settings") rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name") serviceEnvDesc := `Sets extra environment variables for the service. ` + diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index f67b294d4..50fb35d5e 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error { } } - serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled) + serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled) if err := serverInstance.Start(); err != nil { log.Fatalf("failed to start daemon: %v", err) } diff --git a/client/cmd/service_installer.go b/client/cmd/service_installer.go index 92f935d60..293a276dc 100644 --- a/client/cmd/service_installer.go +++ b/client/cmd/service_installer.go @@ -49,6 +49,14 @@ func buildServiceArguments() []string { args = append(args, "--log-file", logFile) } + if profilesDisabled { + args = append(args, "--disable-profiles") + } + + if updateSettingsDisabled { + args = append(args, "--disable-update-settings") + } + return args } diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go index 50986508d..37bdc3018 100644 --- a/client/cmd/testutil_test.go +++ b/client/cmd/testutil_test.go @@ -27,8 +27,8 @@ import ( clientProto "github.com/netbirdio/netbird/client/proto" client "github.com/netbirdio/netbird/client/server" - mgmtProto "github.com/netbirdio/netbird/shared/management/proto" mgmt "github.com/netbirdio/netbird/management/server" + mgmtProto "github.com/netbirdio/netbird/shared/management/proto" sigProto "github.com/netbirdio/netbird/shared/signal/proto" sig "github.com/netbirdio/netbird/signal/server" ) @@ -136,7 +136,7 @@ func startClientDaemon( s := grpc.NewServer() server := client.New(ctx, - "", "", false) + "", "", false, false) if err := server.Start(); err != nil { t.Fatal(err) } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 691976971..60835d1cd 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -4430,6 +4430,94 @@ func (*LogoutResponse) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{66} } +type GetFeaturesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFeaturesRequest) Reset() { + *x = GetFeaturesRequest{} + mi := &file_daemon_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFeaturesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFeaturesRequest) ProtoMessage() {} + +func (x *GetFeaturesRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[67] + 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 GetFeaturesRequest.ProtoReflect.Descriptor instead. +func (*GetFeaturesRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{67} +} + +type GetFeaturesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + DisableProfiles bool `protobuf:"varint,1,opt,name=disable_profiles,json=disableProfiles,proto3" json:"disable_profiles,omitempty"` + DisableUpdateSettings bool `protobuf:"varint,2,opt,name=disable_update_settings,json=disableUpdateSettings,proto3" json:"disable_update_settings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFeaturesResponse) Reset() { + *x = GetFeaturesResponse{} + mi := &file_daemon_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFeaturesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFeaturesResponse) ProtoMessage() {} + +func (x *GetFeaturesResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[68] + 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 GetFeaturesResponse.ProtoReflect.Descriptor instead. +func (*GetFeaturesResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{68} +} + +func (x *GetFeaturesResponse) GetDisableProfiles() bool { + if x != nil { + return x.DisableProfiles + } + return false +} + +func (x *GetFeaturesResponse) GetDisableUpdateSettings() bool { + if x != nil { + return x.DisableUpdateSettings + } + return false +} + type PortInfo_Range struct { state protoimpl.MessageState `protogen:"open.v1"` Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` @@ -4440,7 +4528,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} - mi := &file_daemon_proto_msgTypes[68] + mi := &file_daemon_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4452,7 +4540,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[68] + mi := &file_daemon_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4872,7 +4960,11 @@ const file_daemon_proto_rawDesc = "" + "\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01B\x0e\n" + "\f_profileNameB\v\n" + "\t_username\"\x10\n" + - "\x0eLogoutResponse*b\n" + + "\x0eLogoutResponse\"\x14\n" + + "\x12GetFeaturesRequest\"x\n" + + "\x13GetFeaturesResponse\x12)\n" + + "\x10disable_profiles\x18\x01 \x01(\bR\x0fdisableProfiles\x126\n" + + "\x17disable_update_settings\x18\x02 \x01(\bR\x15disableUpdateSettings*b\n" + "\bLogLevel\x12\v\n" + "\aUNKNOWN\x10\x00\x12\t\n" + "\x05PANIC\x10\x01\x12\t\n" + @@ -4881,7 +4973,7 @@ const file_daemon_proto_rawDesc = "" + "\x04WARN\x10\x04\x12\b\n" + "\x04INFO\x10\x05\x12\t\n" + "\x05DEBUG\x10\x06\x12\t\n" + - "\x05TRACE\x10\a2\xc5\x0f\n" + + "\x05TRACE\x10\a2\x8f\x10\n" + "\rDaemonService\x126\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" + @@ -4912,7 +5004,8 @@ const file_daemon_proto_rawDesc = "" + "\rRemoveProfile\x12\x1c.daemon.RemoveProfileRequest\x1a\x1d.daemon.RemoveProfileResponse\"\x00\x12K\n" + "\fListProfiles\x12\x1b.daemon.ListProfilesRequest\x1a\x1c.daemon.ListProfilesResponse\"\x00\x12W\n" + "\x10GetActiveProfile\x12\x1f.daemon.GetActiveProfileRequest\x1a .daemon.GetActiveProfileResponse\"\x00\x129\n" + - "\x06Logout\x12\x15.daemon.LogoutRequest\x1a\x16.daemon.LogoutResponse\"\x00B\bZ\x06/protob\x06proto3" + "\x06Logout\x12\x15.daemon.LogoutRequest\x1a\x16.daemon.LogoutResponse\"\x00\x12H\n" + + "\vGetFeatures\x12\x1a.daemon.GetFeaturesRequest\x1a\x1b.daemon.GetFeaturesResponse\"\x00B\bZ\x06/protob\x06proto3" var ( file_daemon_proto_rawDescOnce sync.Once @@ -4927,7 +5020,7 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 70) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 72) var file_daemon_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (SystemEvent_Severity)(0), // 1: daemon.SystemEvent.Severity @@ -4999,18 +5092,20 @@ var file_daemon_proto_goTypes = []any{ (*GetActiveProfileResponse)(nil), // 67: daemon.GetActiveProfileResponse (*LogoutRequest)(nil), // 68: daemon.LogoutRequest (*LogoutResponse)(nil), // 69: daemon.LogoutResponse - nil, // 70: daemon.Network.ResolvedIPsEntry - (*PortInfo_Range)(nil), // 71: daemon.PortInfo.Range - nil, // 72: daemon.SystemEvent.MetadataEntry - (*durationpb.Duration)(nil), // 73: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 74: google.protobuf.Timestamp + (*GetFeaturesRequest)(nil), // 70: daemon.GetFeaturesRequest + (*GetFeaturesResponse)(nil), // 71: daemon.GetFeaturesResponse + nil, // 72: daemon.Network.ResolvedIPsEntry + (*PortInfo_Range)(nil), // 73: daemon.PortInfo.Range + nil, // 74: daemon.SystemEvent.MetadataEntry + (*durationpb.Duration)(nil), // 75: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 76: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 73, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 75, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 22, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 74, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 74, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 73, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 76, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 76, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 75, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration 19, // 5: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 18, // 6: daemon.FullStatus.signalState:type_name -> daemon.SignalState 17, // 7: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState @@ -5019,8 +5114,8 @@ var file_daemon_proto_depIdxs = []int32{ 21, // 10: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState 52, // 11: daemon.FullStatus.events:type_name -> daemon.SystemEvent 28, // 12: daemon.ListNetworksResponse.routes:type_name -> daemon.Network - 70, // 13: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry - 71, // 14: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range + 72, // 13: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry + 73, // 14: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range 29, // 15: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo 29, // 16: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo 30, // 17: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule @@ -5031,10 +5126,10 @@ var file_daemon_proto_depIdxs = []int32{ 49, // 22: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage 1, // 23: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity 2, // 24: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category - 74, // 25: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp - 72, // 26: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry + 76, // 25: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp + 74, // 26: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry 52, // 27: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent - 73, // 28: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 75, // 28: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 65, // 29: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile 27, // 30: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList 4, // 31: daemon.DaemonService.Login:input_type -> daemon.LoginRequest @@ -5064,35 +5159,37 @@ var file_daemon_proto_depIdxs = []int32{ 63, // 55: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest 66, // 56: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest 68, // 57: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest - 5, // 58: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 7, // 59: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 9, // 60: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 11, // 61: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 13, // 62: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 15, // 63: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 24, // 64: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse - 26, // 65: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse - 26, // 66: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse - 31, // 67: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse - 33, // 68: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 35, // 69: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 37, // 70: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 40, // 71: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse - 42, // 72: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse - 44, // 73: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 46, // 74: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse - 50, // 75: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse - 52, // 76: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent - 54, // 77: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse - 56, // 78: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse - 58, // 79: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse - 60, // 80: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse - 62, // 81: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse - 64, // 82: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse - 67, // 83: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse - 69, // 84: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse - 58, // [58:85] is the sub-list for method output_type - 31, // [31:58] is the sub-list for method input_type + 70, // 58: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest + 5, // 59: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 7, // 60: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 9, // 61: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 11, // 62: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 13, // 63: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 15, // 64: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 24, // 65: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse + 26, // 66: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse + 26, // 67: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse + 31, // 68: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse + 33, // 69: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 35, // 70: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 37, // 71: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 40, // 72: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 42, // 73: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 44, // 74: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 46, // 75: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse + 50, // 76: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse + 52, // 77: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent + 54, // 78: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse + 56, // 79: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse + 58, // 80: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse + 60, // 81: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse + 62, // 82: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse + 64, // 83: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse + 67, // 84: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse + 69, // 85: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse + 71, // 86: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse + 59, // [59:87] is the sub-list for method output_type + 31, // [31:59] is the sub-list for method input_type 31, // [31:31] is the sub-list for extension type_name 31, // [31:31] is the sub-list for extension extendee 0, // [0:31] is the sub-list for field type_name @@ -5120,7 +5217,7 @@ func file_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)), NumEnums: 3, - NumMessages: 70, + NumMessages: 72, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 76db56459..fa54071ec 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -82,6 +82,8 @@ service DaemonService { // Logout disconnects from the network and deletes the peer from the management server rpc Logout(LogoutRequest) returns (LogoutResponse) {} + + rpc GetFeatures(GetFeaturesRequest) returns (GetFeaturesResponse) {} } @@ -624,4 +626,11 @@ message LogoutRequest { optional string username = 2; } -message LogoutResponse {} \ No newline at end of file +message LogoutResponse {} + +message GetFeaturesRequest{} + +message GetFeaturesResponse{ + bool disable_profiles = 1; + bool disable_update_settings = 2; +} \ No newline at end of file diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 6dfdfa9c3..bf7c9c7b3 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -63,6 +63,7 @@ type DaemonServiceClient interface { GetActiveProfile(ctx context.Context, in *GetActiveProfileRequest, opts ...grpc.CallOption) (*GetActiveProfileResponse, error) // Logout disconnects from the network and deletes the peer from the management server Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) + GetFeatures(ctx context.Context, in *GetFeaturesRequest, opts ...grpc.CallOption) (*GetFeaturesResponse, error) } type daemonServiceClient struct { @@ -339,6 +340,15 @@ func (c *daemonServiceClient) Logout(ctx context.Context, in *LogoutRequest, opt return out, nil } +func (c *daemonServiceClient) GetFeatures(ctx context.Context, in *GetFeaturesRequest, opts ...grpc.CallOption) (*GetFeaturesResponse, error) { + out := new(GetFeaturesResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetFeatures", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer // for forward compatibility @@ -388,6 +398,7 @@ type DaemonServiceServer interface { GetActiveProfile(context.Context, *GetActiveProfileRequest) (*GetActiveProfileResponse, error) // Logout disconnects from the network and deletes the peer from the management server Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) + GetFeatures(context.Context, *GetFeaturesRequest) (*GetFeaturesResponse, error) mustEmbedUnimplementedDaemonServiceServer() } @@ -476,6 +487,9 @@ func (UnimplementedDaemonServiceServer) GetActiveProfile(context.Context, *GetAc func (UnimplementedDaemonServiceServer) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented") } +func (UnimplementedDaemonServiceServer) GetFeatures(context.Context, *GetFeaturesRequest) (*GetFeaturesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetFeatures not implemented") +} func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service. @@ -978,6 +992,24 @@ func _DaemonService_Logout_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } +func _DaemonService_GetFeatures_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetFeaturesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).GetFeatures(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/GetFeatures", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).GetFeatures(ctx, req.(*GetFeaturesRequest)) + } + return interceptor(ctx, in, info, handler) +} + // DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1089,6 +1121,10 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Logout", Handler: _DaemonService_Logout_Handler, }, + { + MethodName: "GetFeatures", + Handler: _DaemonService_GetFeatures_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/client/server/server.go b/client/server/server.go index daef7d02b..f2e8dc12a 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -46,8 +46,9 @@ const ( defaultMaxRetryTime = 14 * 24 * time.Hour defaultRetryMultiplier = 1.7 - errRestoreResidualState = "failed to restore residual state: %v" - errProfilesDisabled = "profiles are disabled, you cannot use this feature without profiles enabled" + errRestoreResidualState = "failed to restore residual state: %v" + errProfilesDisabled = "profiles are disabled, you cannot use this feature without profiles enabled" + errUpdateSettingsDisabled = "update settings are disabled, you cannot use this feature without update settings enabled" ) var ErrServiceNotUp = errors.New("service is not up") @@ -74,8 +75,9 @@ type Server struct { persistSyncResponse bool isSessionActive atomic.Bool - profileManager *profilemanager.ServiceManager - profilesDisabled bool + profileManager *profilemanager.ServiceManager + profilesDisabled bool + updateSettingsDisabled bool } type oauthAuthFlow struct { @@ -86,14 +88,15 @@ type oauthAuthFlow struct { } // New server instance constructor. -func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool) *Server { +func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool, updateSettingsDisabled bool) *Server { return &Server{ - rootCtx: ctx, - logFile: logFile, - persistSyncResponse: true, - statusRecorder: peer.NewRecorder(""), - profileManager: profilemanager.NewServiceManager(configFile), - profilesDisabled: profilesDisabled, + rootCtx: ctx, + logFile: logFile, + persistSyncResponse: true, + statusRecorder: peer.NewRecorder(""), + profileManager: profilemanager.NewServiceManager(configFile), + profilesDisabled: profilesDisabled, + updateSettingsDisabled: updateSettingsDisabled, } } @@ -322,8 +325,8 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques s.mutex.Lock() defer s.mutex.Unlock() - if s.checkProfilesDisabled() { - return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled) + if s.checkUpdateSettingsDisabled() { + return nil, gstatus.Errorf(codes.Unavailable, errUpdateSettingsDisabled) } profState := profilemanager.ActiveProfileState{ @@ -1330,10 +1333,31 @@ func (s *Server) GetActiveProfile(ctx context.Context, msg *proto.GetActiveProfi }, nil } +// GetFeatures returns the features supported by the daemon. +func (s *Server) GetFeatures(ctx context.Context, msg *proto.GetFeaturesRequest) (*proto.GetFeaturesResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + features := &proto.GetFeaturesResponse{ + DisableProfiles: s.checkProfilesDisabled(), + DisableUpdateSettings: s.checkUpdateSettingsDisabled(), + } + + return features, nil +} + func (s *Server) checkProfilesDisabled() bool { // Check if the environment variable is set to disable profiles if s.profilesDisabled { - log.Warn("Profiles are disabled via NB_DISABLE_PROFILES environment variable") + return true + } + + return false +} + +func (s *Server) checkUpdateSettingsDisabled() bool { + // Check if the environment variable is set to disable profiles + if s.updateSettingsDisabled { return true } diff --git a/client/server/server_test.go b/client/server/server_test.go index 8e4e0e687..24c3cbcf5 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -25,7 +25,6 @@ import ( "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/shared/management/proto" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" @@ -34,6 +33,7 @@ import ( "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" + mgmtProto "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/shared/signal/proto" signalServer "github.com/netbirdio/netbird/signal/server" ) @@ -95,7 +95,7 @@ func TestConnectWithRetryRuns(t *testing.T) { t.Fatalf("failed to set active profile state: %v", err) } - s := New(ctx, "debug", "", false) + s := New(ctx, "debug", "", false, false) s.config = config @@ -152,7 +152,7 @@ func TestServer_Up(t *testing.T) { t.Fatalf("failed to set active profile state: %v", err) } - s := New(ctx, "console", "", false) + s := New(ctx, "console", "", false, false) err = s.Start() require.NoError(t, err) @@ -228,7 +228,7 @@ func TestServer_SubcribeEvents(t *testing.T) { t.Fatalf("failed to set active profile state: %v", err) } - s := New(ctx, "console", "", false) + s := New(ctx, "console", "", false, false) err = s.Start() require.NoError(t, err) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 88cb11eab..f43606de1 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -392,6 +392,16 @@ func (s *serviceClient) updateIcon() { } func (s *serviceClient) showSettingsUI() { + // Check if update settings are disabled by daemon + features, err := s.getFeatures() + if err != nil { + log.Errorf("failed to get features from daemon: %v", err) + // Continue with default behavior if features can't be retrieved + } else if features != nil && features.DisableUpdateSettings { + log.Warn("Update settings are disabled by daemon") + return + } + // add settings window UI elements. s.wSettings = s.app.NewWindow("NetBird Settings") s.wSettings.SetOnClosed(s.cancel) @@ -447,6 +457,17 @@ func (s *serviceClient) getSettingsForm() *widget.Form { }, SubmitText: "Save", OnSubmit: func() { + // Check if update settings are disabled by daemon + features, err := s.getFeatures() + if err != nil { + log.Errorf("failed to get features from daemon: %v", err) + // Continue with default behavior if features can't be retrieved + } else if features != nil && features.DisableUpdateSettings { + log.Warn("Configuration updates are disabled by daemon") + dialog.ShowError(fmt.Errorf("Configuration updates are disabled by daemon"), s.wSettings) + return + } + if s.iPreSharedKey.Text != "" && s.iPreSharedKey.Text != censoredPreSharedKey { // validate preSharedKey if it added if _, err := wgtypes.ParseKey(s.iPreSharedKey.Text); err != nil { @@ -836,6 +857,20 @@ func (s *serviceClient) onTrayReady() { s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", debugBundleMenuDescr) s.loadSettings() + // Disable settings menu if update settings are disabled by daemon + features, err := s.getFeatures() + if err != nil { + log.Errorf("failed to get features from daemon: %v", err) + // Continue with default behavior if features can't be retrieved + } else { + if features != nil && features.DisableUpdateSettings { + s.setSettingsEnabled(false) + } + if features != nil && features.DisableProfiles { + s.mProfile.setEnabled(false) + } + } + s.exitNodeMu.Lock() s.mExitNode = systray.AddMenuItem("Exit Node", exitNodeMenuDescr) s.mExitNode.Disable() @@ -876,6 +911,10 @@ func (s *serviceClient) onTrayReady() { if err != nil { log.Errorf("error while updating status: %v", err) } + + // Check features periodically to handle daemon restarts + s.checkAndUpdateFeatures() + time.Sleep(2 * time.Second) } }() @@ -948,6 +987,59 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService return s.conn, nil } +// setSettingsEnabled enables or disables the settings menu based on the provided state +func (s *serviceClient) setSettingsEnabled(enabled bool) { + if s.mSettings != nil { + if enabled { + s.mSettings.Enable() + s.mSettings.SetTooltip(settingsMenuDescr) + } else { + s.mSettings.Hide() + s.mSettings.SetTooltip("Settings are disabled by daemon") + } + } +} + +// checkAndUpdateFeatures checks the current features and updates the UI accordingly +func (s *serviceClient) checkAndUpdateFeatures() { + features, err := s.getFeatures() + if err != nil { + log.Errorf("failed to get features from daemon: %v", err) + return + } + + // Update settings menu based on current features + if features != nil && features.DisableUpdateSettings { + s.setSettingsEnabled(false) + } else { + s.setSettingsEnabled(true) + } + + // Update profile menu based on current features + if s.mProfile != nil { + if features != nil && features.DisableProfiles { + s.mProfile.setEnabled(false) + } else { + s.mProfile.setEnabled(true) + } + } +} + +// getFeatures from the daemon to determine which features are enabled/disabled. +func (s *serviceClient) getFeatures() (*proto.GetFeaturesResponse, error) { + conn, err := s.getSrvClient(failFastTimeout) + if err != nil { + return nil, fmt.Errorf("get client for features: %w", err) + } + + features, err := conn.GetFeatures(s.ctx, &proto.GetFeaturesRequest{}) + if err != nil { + return nil, fmt.Errorf("get features from daemon: %w", err) + } + + return features, nil +} + // getSrvConfig from the service to show it in the settings window. func (s *serviceClient) getSrvConfig() { s.managementURL = profilemanager.DefaultManagementURL diff --git a/client/ui/profile.go b/client/ui/profile.go index f4505ab19..075223795 100644 --- a/client/ui/profile.go +++ b/client/ui/profile.go @@ -654,6 +654,19 @@ func (p *profileMenu) clear(profiles []Profile) { } } +// setEnabled enables or disables the profile menu based on the provided state +func (p *profileMenu) setEnabled(enabled bool) { + if p.profileMenuItem != nil { + if enabled { + p.profileMenuItem.Enable() + p.profileMenuItem.SetTooltip("") + } else { + p.profileMenuItem.Hide() + p.profileMenuItem.SetTooltip("Profiles are disabled by daemon") + } + } +} + func (p *profileMenu) updateMenu() { // check every second ticker := time.NewTicker(time.Second) @@ -662,7 +675,6 @@ func (p *profileMenu) updateMenu() { for { select { case <-ticker.C: - // get profilesList profiles, err := p.getProfiles() if err != nil {