[client] Add --disable-networks flag to block network selection (#5896)

This commit is contained in:
Viktor Liu
2026-04-16 21:02:31 +09:00
committed by GitHub
parent 08f624507d
commit 6b540d145c
14 changed files with 69 additions and 14 deletions

View File

@@ -75,6 +75,7 @@ var (
mtu uint16 mtu uint16
profilesDisabled bool profilesDisabled bool
updateSettingsDisabled bool updateSettingsDisabled bool
networksDisabled bool
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "netbird", Use: "netbird",

View File

@@ -44,6 +44,7 @@ func init() {
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd, resetParamsCmd) serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd, resetParamsCmd)
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(&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") 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")
serviceCmd.PersistentFlags().BoolVar(&networksDisabled, "disable-networks", false, "Disables network selection. If enabled, the client will not allow listing, selecting, or deselecting networks. To persist, use: netbird service install --disable-networks")
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name") rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
serviceEnvDesc := `Sets extra environment variables for the service. ` + serviceEnvDesc := `Sets extra environment variables for the service. ` +

View File

@@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error {
} }
} }
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled) serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, networksDisabled)
if err := serverInstance.Start(); err != nil { if err := serverInstance.Start(); err != nil {
log.Fatalf("failed to start daemon: %v", err) log.Fatalf("failed to start daemon: %v", err)
} }

View File

@@ -59,6 +59,10 @@ func buildServiceArguments() []string {
args = append(args, "--disable-update-settings") args = append(args, "--disable-update-settings")
} }
if networksDisabled {
args = append(args, "--disable-networks")
}
return args return args
} }

View File

@@ -28,6 +28,7 @@ type serviceParams struct {
LogFiles []string `json:"log_files,omitempty"` LogFiles []string `json:"log_files,omitempty"`
DisableProfiles bool `json:"disable_profiles,omitempty"` DisableProfiles bool `json:"disable_profiles,omitempty"`
DisableUpdateSettings bool `json:"disable_update_settings,omitempty"` DisableUpdateSettings bool `json:"disable_update_settings,omitempty"`
DisableNetworks bool `json:"disable_networks,omitempty"`
ServiceEnvVars map[string]string `json:"service_env_vars,omitempty"` ServiceEnvVars map[string]string `json:"service_env_vars,omitempty"`
} }
@@ -78,6 +79,7 @@ func currentServiceParams() *serviceParams {
LogFiles: logFiles, LogFiles: logFiles,
DisableProfiles: profilesDisabled, DisableProfiles: profilesDisabled,
DisableUpdateSettings: updateSettingsDisabled, DisableUpdateSettings: updateSettingsDisabled,
DisableNetworks: networksDisabled,
} }
if len(serviceEnvVars) > 0 { if len(serviceEnvVars) > 0 {
@@ -142,6 +144,10 @@ func applyServiceParams(cmd *cobra.Command, params *serviceParams) {
updateSettingsDisabled = params.DisableUpdateSettings updateSettingsDisabled = params.DisableUpdateSettings
} }
if !serviceCmd.PersistentFlags().Changed("disable-networks") {
networksDisabled = params.DisableNetworks
}
applyServiceEnvParams(cmd, params) applyServiceEnvParams(cmd, params)
} }

View File

@@ -535,6 +535,7 @@ func fieldToGlobalVar(field string) string {
"LogFiles": "logFiles", "LogFiles": "logFiles",
"DisableProfiles": "profilesDisabled", "DisableProfiles": "profilesDisabled",
"DisableUpdateSettings": "updateSettingsDisabled", "DisableUpdateSettings": "updateSettingsDisabled",
"DisableNetworks": "networksDisabled",
"ServiceEnvVars": "serviceEnvVars", "ServiceEnvVars": "serviceEnvVars",
} }
if v, ok := m[field]; ok { if v, ok := m[field]; ok {

View File

@@ -152,7 +152,7 @@ func startClientDaemon(
s := grpc.NewServer() s := grpc.NewServer()
server := client.New(ctx, server := client.New(ctx,
"", "", false, false) "", "", false, false, false)
if err := server.Start(); err != nil { if err := server.Start(); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -4979,6 +4979,7 @@ type GetFeaturesResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
DisableProfiles bool `protobuf:"varint,1,opt,name=disable_profiles,json=disableProfiles,proto3" json:"disable_profiles,omitempty"` 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"` DisableUpdateSettings bool `protobuf:"varint,2,opt,name=disable_update_settings,json=disableUpdateSettings,proto3" json:"disable_update_settings,omitempty"`
DisableNetworks bool `protobuf:"varint,3,opt,name=disable_networks,json=disableNetworks,proto3" json:"disable_networks,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -5027,6 +5028,13 @@ func (x *GetFeaturesResponse) GetDisableUpdateSettings() bool {
return false return false
} }
func (x *GetFeaturesResponse) GetDisableNetworks() bool {
if x != nil {
return x.DisableNetworks
}
return false
}
type TriggerUpdateRequest struct { type TriggerUpdateRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@@ -6472,10 +6480,11 @@ const file_daemon_proto_rawDesc = "" +
"\f_profileNameB\v\n" + "\f_profileNameB\v\n" +
"\t_username\"\x10\n" + "\t_username\"\x10\n" +
"\x0eLogoutResponse\"\x14\n" + "\x0eLogoutResponse\"\x14\n" +
"\x12GetFeaturesRequest\"x\n" + "\x12GetFeaturesRequest\"\xa3\x01\n" +
"\x13GetFeaturesResponse\x12)\n" + "\x13GetFeaturesResponse\x12)\n" +
"\x10disable_profiles\x18\x01 \x01(\bR\x0fdisableProfiles\x126\n" + "\x10disable_profiles\x18\x01 \x01(\bR\x0fdisableProfiles\x126\n" +
"\x17disable_update_settings\x18\x02 \x01(\bR\x15disableUpdateSettings\"\x16\n" + "\x17disable_update_settings\x18\x02 \x01(\bR\x15disableUpdateSettings\x12)\n" +
"\x10disable_networks\x18\x03 \x01(\bR\x0fdisableNetworks\"\x16\n" +
"\x14TriggerUpdateRequest\"M\n" + "\x14TriggerUpdateRequest\"M\n" +
"\x15TriggerUpdateResponse\x12\x18\n" + "\x15TriggerUpdateResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1a\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1a\n" +

View File

@@ -727,6 +727,7 @@ message GetFeaturesRequest{}
message GetFeaturesResponse{ message GetFeaturesResponse{
bool disable_profiles = 1; bool disable_profiles = 1;
bool disable_update_settings = 2; bool disable_update_settings = 2;
bool disable_networks = 3;
} }
message TriggerUpdateRequest {} message TriggerUpdateRequest {}

View File

@@ -9,6 +9,8 @@ import (
"strings" "strings"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/route"
@@ -27,6 +29,10 @@ func (s *Server) ListNetworks(context.Context, *proto.ListNetworksRequest) (*pro
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if s.networksDisabled {
return nil, gstatus.Errorf(codes.Unavailable, errNetworksDisabled)
}
if s.connectClient == nil { if s.connectClient == nil {
return nil, fmt.Errorf("not connected") return nil, fmt.Errorf("not connected")
} }
@@ -118,6 +124,10 @@ func (s *Server) SelectNetworks(_ context.Context, req *proto.SelectNetworksRequ
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if s.networksDisabled {
return nil, gstatus.Errorf(codes.Unavailable, errNetworksDisabled)
}
if s.connectClient == nil { if s.connectClient == nil {
return nil, fmt.Errorf("not connected") return nil, fmt.Errorf("not connected")
} }
@@ -164,6 +174,10 @@ func (s *Server) DeselectNetworks(_ context.Context, req *proto.SelectNetworksRe
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if s.networksDisabled {
return nil, gstatus.Errorf(codes.Unavailable, errNetworksDisabled)
}
if s.connectClient == nil { if s.connectClient == nil {
return nil, fmt.Errorf("not connected") return nil, fmt.Errorf("not connected")
} }

View File

@@ -53,6 +53,7 @@ const (
errRestoreResidualState = "failed to restore residual state: %v" errRestoreResidualState = "failed to restore residual state: %v"
errProfilesDisabled = "profiles are disabled, you cannot use this feature without profiles enabled" 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" errUpdateSettingsDisabled = "update settings are disabled, you cannot use this feature without update settings enabled"
errNetworksDisabled = "network selection is disabled by the administrator"
) )
var ErrServiceNotUp = errors.New("service is not up") var ErrServiceNotUp = errors.New("service is not up")
@@ -88,6 +89,7 @@ type Server struct {
profileManager *profilemanager.ServiceManager profileManager *profilemanager.ServiceManager
profilesDisabled bool profilesDisabled bool
updateSettingsDisabled bool updateSettingsDisabled bool
networksDisabled bool
sleepHandler *sleephandler.SleepHandler sleepHandler *sleephandler.SleepHandler
@@ -104,7 +106,7 @@ type oauthAuthFlow struct {
} }
// New server instance constructor. // New server instance constructor.
func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool, updateSettingsDisabled bool) *Server { func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool, updateSettingsDisabled bool, networksDisabled bool) *Server {
s := &Server{ s := &Server{
rootCtx: ctx, rootCtx: ctx,
logFile: logFile, logFile: logFile,
@@ -113,6 +115,7 @@ func New(ctx context.Context, logFile string, configFile string, profilesDisable
profileManager: profilemanager.NewServiceManager(configFile), profileManager: profilemanager.NewServiceManager(configFile),
profilesDisabled: profilesDisabled, profilesDisabled: profilesDisabled,
updateSettingsDisabled: updateSettingsDisabled, updateSettingsDisabled: updateSettingsDisabled,
networksDisabled: networksDisabled,
jwtCache: newJWTCache(), jwtCache: newJWTCache(),
} }
agent := &serverAgent{s} agent := &serverAgent{s}
@@ -1628,6 +1631,7 @@ func (s *Server) GetFeatures(ctx context.Context, msg *proto.GetFeaturesRequest)
features := &proto.GetFeaturesResponse{ features := &proto.GetFeaturesResponse{
DisableProfiles: s.checkProfilesDisabled(), DisableProfiles: s.checkProfilesDisabled(),
DisableUpdateSettings: s.checkUpdateSettingsDisabled(), DisableUpdateSettings: s.checkUpdateSettingsDisabled(),
DisableNetworks: s.networksDisabled,
} }
return features, nil return features, nil

View File

@@ -103,7 +103,7 @@ func TestConnectWithRetryRuns(t *testing.T) {
t.Fatalf("failed to set active profile state: %v", err) t.Fatalf("failed to set active profile state: %v", err)
} }
s := New(ctx, "debug", "", false, false) s := New(ctx, "debug", "", false, false, false)
s.config = config s.config = config
@@ -164,7 +164,7 @@ func TestServer_Up(t *testing.T) {
t.Fatalf("failed to set active profile state: %v", err) t.Fatalf("failed to set active profile state: %v", err)
} }
s := New(ctx, "console", "", false, false) s := New(ctx, "console", "", false, false, false)
err = s.Start() err = s.Start()
require.NoError(t, err) require.NoError(t, err)
@@ -234,7 +234,7 @@ func TestServer_SubcribeEvents(t *testing.T) {
t.Fatalf("failed to set active profile state: %v", err) t.Fatalf("failed to set active profile state: %v", err)
} }
s := New(ctx, "console", "", false, false) s := New(ctx, "console", "", false, false, false)
err = s.Start() err = s.Start()
require.NoError(t, err) require.NoError(t, err)

View File

@@ -53,7 +53,7 @@ func TestSetConfig_AllFieldsSaved(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
ctx := context.Background() ctx := context.Background()
s := New(ctx, "console", "", false, false) s := New(ctx, "console", "", false, false, false)
rosenpassEnabled := true rosenpassEnabled := true
rosenpassPermissive := true rosenpassPermissive := true

View File

@@ -314,6 +314,7 @@ type serviceClient struct {
lastNotifiedVersion string lastNotifiedVersion string
settingsEnabled bool settingsEnabled bool
profilesEnabled bool profilesEnabled bool
networksEnabled bool
showNetworks bool showNetworks bool
wNetworks fyne.Window wNetworks fyne.Window
wProfiles fyne.Window wProfiles fyne.Window
@@ -368,6 +369,7 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient {
showAdvancedSettings: args.showSettings, showAdvancedSettings: args.showSettings,
showNetworks: args.showNetworks, showNetworks: args.showNetworks,
networksEnabled: true,
} }
s.eventHandler = newEventHandler(s) s.eventHandler = newEventHandler(s)
@@ -920,8 +922,10 @@ func (s *serviceClient) updateStatus() error {
s.mStatus.SetIcon(s.icConnectedDot) s.mStatus.SetIcon(s.icConnectedDot)
s.mUp.Disable() s.mUp.Disable()
s.mDown.Enable() s.mDown.Enable()
s.mNetworks.Enable() if s.networksEnabled {
s.mExitNode.Enable() s.mNetworks.Enable()
s.mExitNode.Enable()
}
s.startExitNodeRefresh() s.startExitNodeRefresh()
systrayIconState = true systrayIconState = true
case status.Status == string(internal.StatusConnecting): case status.Status == string(internal.StatusConnecting):
@@ -1093,14 +1097,14 @@ func (s *serviceClient) onTrayReady() {
s.getSrvConfig() s.getSrvConfig()
time.Sleep(100 * time.Millisecond) // To prevent race condition caused by systray not being fully initialized and ignoring setIcon time.Sleep(100 * time.Millisecond) // To prevent race condition caused by systray not being fully initialized and ignoring setIcon
for { for {
// Check features before status so menus respect disable flags before being enabled
s.checkAndUpdateFeatures()
err := s.updateStatus() err := s.updateStatus()
if err != nil { if err != nil {
log.Errorf("error while updating status: %v", err) log.Errorf("error while updating status: %v", err)
} }
// Check features periodically to handle daemon restarts
s.checkAndUpdateFeatures()
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
}() }()
@@ -1299,6 +1303,16 @@ func (s *serviceClient) checkAndUpdateFeatures() {
s.mProfile.setEnabled(profilesEnabled) s.mProfile.setEnabled(profilesEnabled)
} }
} }
// Update networks and exit node menus based on current features
s.networksEnabled = features == nil || !features.DisableNetworks
if s.networksEnabled && s.connected {
s.mNetworks.Enable()
s.mExitNode.Enable()
} else {
s.mNetworks.Disable()
s.mExitNode.Disable()
}
} }
// getFeatures from the daemon to determine which features are enabled/disabled. // getFeatures from the daemon to determine which features are enabled/disabled.