From b55856307fb7d0f1b25f026987b25b94a7135739 Mon Sep 17 00:00:00 2001 From: Bartosz Oleaczek Date: Wed, 20 Nov 2024 14:01:36 +0100 Subject: [PATCH] Hide quench technology based on compile flag and remote config --- .gitmodules | 2 +- ci/generate_protobuf.sh | 2 + ci/test.sh | 2 +- cli/cli.go | 8 +- cli/cli_set_technology.go | 7 +- cmd/daemon/firebase_cfg.go | 12 +++ cmd/daemon/main.go | 21 ++++- cmd/daemon/no_firebase_cfg.go | 25 ++++++ cmd/daemon/vpn_factory.go | 2 +- cmd/daemon/vpn_no_quench.go | 10 ++- cmd/daemon/vpn_quench.go | 4 +- cmd/daemon/vpnconfig_linux.go | 3 +- cmd/daemon/vpnconfig_telio.go | 3 +- config/remote/firebase.go | 39 +++++++- config/remote/firebase_test.go | 12 +-- config/remote/remote_config.go | 5 +- daemon/pb/quench_hidden.pb.go | 128 +++++++++++++++++++++++++++ daemon/pb/service_grpc.pb.go | 38 ++++++++ daemon/rpc.go | 52 ++++++----- daemon/rpc_connect.go | 30 +++++++ daemon/rpc_is_quench_enabled.go | 23 +++++ daemon/rpc_is_quench_enabled_test.go | 42 +++++++++ daemon/rpc_set_technology.go | 26 ++++++ daemon/rpc_set_technology_test.go | 45 ++++++++++ daemon/rpc_test.go | 1 + features/no_quench.go | 5 ++ features/quench.go | 5 ++ internal/codes.go | 1 + protobuf/daemon/quench_hidden.proto | 9 ++ protobuf/daemon/service.proto | 2 + test/mock/remote_config.go | 18 ++++ 31 files changed, 531 insertions(+), 51 deletions(-) create mode 100644 cmd/daemon/firebase_cfg.go create mode 100644 cmd/daemon/no_firebase_cfg.go create mode 100644 daemon/pb/quench_hidden.pb.go create mode 100644 daemon/rpc_is_quench_enabled.go create mode 100644 daemon/rpc_is_quench_enabled_test.go create mode 100644 daemon/rpc_set_technology_test.go create mode 100644 features/no_quench.go create mode 100644 features/quench.go create mode 100644 protobuf/daemon/quench_hidden.proto create mode 100644 test/mock/remote_config.go diff --git a/.gitmodules b/.gitmodules index ef3460c6..5a7de9c4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,5 +5,5 @@ path = third-party/moose-worker url = ../../../../moose/moose-events.git [submodule "third-party/libquench-go"] + url = ../../libquench-go.git path = third-party/libquench-go - url = ../../libquench-go diff --git a/ci/generate_protobuf.sh b/ci/generate_protobuf.sh index 9e70408c..5429c7df 100755 --- a/ci/generate_protobuf.sh +++ b/ci/generate_protobuf.sh @@ -32,6 +32,8 @@ protoc --go_opt=module=github.com/NordSecurity/nordvpn-linux --go_out=. protobuf protoc --go_opt=module=github.com/NordSecurity/nordvpn-linux --go_out=. protobuf/fileshare/fileshare.proto -I protobuf/fileshare protoc --go_opt=module=github.com/NordSecurity/nordvpn-linux --go_out=. protobuf/snapconf/snapconf.proto -I protobuf/snapconf protoc --go_opt=module=github.com/NordSecurity/nordvpn-linux --go_out=. protobuf/norduser/norduser.proto -I protobuf/norduser +protoc --go_opt=module=github.com/NordSecurity/nordvpn-linux --go_out=. protobuf/daemon/state.proto -I protobuf/daemon +protoc --go_opt=module=github.com/NordSecurity/nordvpn-linux --go_out=. protobuf/daemon/quench_hidden.proto -I protobuf/daemon protoc --go_grpc_opt=module=github.com/NordSecurity/nordvpn-linux --go_grpc_out=. protobuf/daemon/service.proto -I protobuf/daemon protoc --go_grpc_opt=module=github.com/NordSecurity/nordvpn-linux --go_grpc_out=. protobuf/meshnet/service.proto -I protobuf/meshnet diff --git a/ci/test.sh b/ci/test.sh index 0ca4e5cd..f4582bd0 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -23,7 +23,7 @@ if [ "${1:-""}" = "full" ]; then excluded_packages="thisshouldneverexist" excluded_categories="root,link" - tags="internal,moose" + tags="internal,quench,moose" fi # Execute tests in all the packages except the excluded ones diff --git a/cli/cli.go b/cli/cli.go index 2d697b57..6235dfeb 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -119,6 +119,12 @@ func NewApp(version, environment, hash, salt string, cli.HelpFlag.(*cli.BoolFlag).Usage = "Show help" cli.VersionFlag.(*cli.BoolFlag).Usage = "Print the version" + setTechnologyDescription := fmt.Sprintf(SetTechnologyDescription, SupportedValuesQuench) + quenchEnabled, err := cmd.client.IsQuenchEnabled(context.Background(), &pb.Empty{}) + if err != nil || !quenchEnabled.Enabled { + setTechnologyDescription = fmt.Sprintf(SetTechnologyDescription, SupportedValuesNoQuench) + } + setCommand := cli.Command{ Name: "set", Aliases: []string{"s"}, @@ -285,7 +291,7 @@ func NewApp(version, environment, hash, salt string, Action: cmd.SetTechnology, BashComplete: cmd.SetTechnologyAutoComplete, ArgsUsage: SetTechnologyArgsUsageText, - Description: SetTechnologyDescription, + Description: setTechnologyDescription, }, { Name: "meshnet", diff --git a/cli/cli_set_technology.go b/cli/cli_set_technology.go index f1a79aae..1f80a30f 100644 --- a/cli/cli_set_technology.go +++ b/cli/cli_set_technology.go @@ -19,9 +19,12 @@ const ( SetTechnologyUsageText = "Sets the technology" SetTechnologyArgsUsageText = `` SetTechnologyDescription = `Use this command to set the technology. -Supported values for : OPENVPN or NORDLYNX. +Supported values for : %s. Example: 'nordvpn set technology OPENVPN'` + + SupportedValuesNoQuench = "OPENVPN or NORDLYNX" + SupportedValuesQuench = "OPENVPN, NORDLYNX or QUENCH" ) func (c *cmd) SetTechnology(ctx *cli.Context) error { @@ -69,6 +72,8 @@ func (c *cmd) SetTechnology(ctx *cli.Context) error { // must be right before CodeSuccess color.Yellow(SetAutoConnectForceOff) fallthrough + case internal.CodeFeatureHidden: + return formatError(argsParseError(ctx)) case internal.CodeSuccess: flag, _ := strconv.ParseBool(resp.Data[0]) color.Green(fmt.Sprintf(MsgSetSuccess, "Technology", strings.Join(resp.Data[1:], " "))) diff --git a/cmd/daemon/firebase_cfg.go b/cmd/daemon/firebase_cfg.go new file mode 100644 index 00000000..5ed6b398 --- /dev/null +++ b/cmd/daemon/firebase_cfg.go @@ -0,0 +1,12 @@ +//go:build firebase + +package main + +import ( + "github.com/NordSecurity/nordvpn-linux/config" + "github.com/NordSecurity/nordvpn-linux/config/remote" +) + +func getRemoteConfigGetter(cm config.Manager) remote.RemoteConfigGetter { + return remote.NewRConfig(remote.UpdatePeriod, remote.NewFirebaseService(FirebaseToken), cm) +} diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index c265ecda..634bc649 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -49,6 +49,7 @@ import ( "github.com/NordSecurity/nordvpn-linux/events/meshunsetter" "github.com/NordSecurity/nordvpn-linux/events/refresher" "github.com/NordSecurity/nordvpn-linux/events/subs" + "github.com/NordSecurity/nordvpn-linux/features" grpcmiddleware "github.com/NordSecurity/nordvpn-linux/grpc_middleware" "github.com/NordSecurity/nordvpn-linux/internal" "github.com/NordSecurity/nordvpn-linux/ipv6" @@ -143,6 +144,22 @@ func main() { } } + rcConfig := getRemoteConfigGetter(fsystem) + quenchEnabled, err := rcConfig.GetQuenchEnabled(Version) + if err != nil { + log.Println("failed to determine if quench is enabled:", err) + } + + // fallback to Nordlynx if quench was enabled in previous installation and is disabled now + if (!features.QuenchEnabled || !quenchEnabled) && cfg.Technology == config.Technology_QUENCH { + err := fsystem.SaveWith(func(c config.Config) config.Config { + c.Technology = config.Technology_NORDLYNX + return c + }) + + log.Println(internal.ErrorPrefix, "failed to fallback to Nordlynx tech:", err) + } + // Events daemonEvents := daemonevents.NewEventsEmpty() @@ -190,7 +207,6 @@ func main() { // API var validator response.Validator - var err error if !internal.IsProdEnv(Environment) && os.Getenv(EnvIgnoreHeaderValidation) == "1" { validator = response.NoopValidator{} } else { @@ -292,7 +308,7 @@ func main() { daemonEvents.Service.UiItemsClick.Publish(events.UiItemsAction{ItemName: "first_open", ItemType: "button", ItemValue: "first_open", FormReference: "daemon"}) } - vpnLibConfigGetter := vpnLibConfigGetterImplementation(fsystem) + vpnLibConfigGetter := vpnLibConfigGetterImplementation(fsystem, rcConfig) internalVpnEvents := vpn.NewInternalVPNEvents() @@ -463,6 +479,7 @@ func main() { meshAPIex, statePublisher, sharedContext, + rcConfig, ) meshService := meshnet.NewServer( authChecker, diff --git a/cmd/daemon/no_firebase_cfg.go b/cmd/daemon/no_firebase_cfg.go new file mode 100644 index 00000000..af652d7c --- /dev/null +++ b/cmd/daemon/no_firebase_cfg.go @@ -0,0 +1,25 @@ +//go:build !firebase + +package main + +import ( + "fmt" + + "github.com/NordSecurity/nordvpn-linux/config" + "github.com/NordSecurity/nordvpn-linux/config/remote" +) + +type RemoteConfigGetterStub struct { +} + +func (r RemoteConfigGetterStub) GetTelioConfig(version string) (string, error) { + return "", fmt.Errorf("firebase config was not compiled into the app") +} + +func (r RemoteConfigGetterStub) GetQuenchEnabled(version string) (bool, error) { + return false, nil +} + +func getRemoteConfigGetter(_ config.Manager) remote.RemoteConfigGetter { + return RemoteConfigGetterStub{} +} diff --git a/cmd/daemon/vpn_factory.go b/cmd/daemon/vpn_factory.go index 73c8170d..9c9fc044 100644 --- a/cmd/daemon/vpn_factory.go +++ b/cmd/daemon/vpn_factory.go @@ -27,7 +27,7 @@ func getVpnFactory(eventsDbPath string, fwmark uint32, envIsDev bool, case config.Technology_OPENVPN: return openvpn.New(fwmark, eventsPublisher), nil case config.Technology_QUENCH: - return getQuenchVPN(fwmark), nil + return getQuenchVPN(fwmark) case config.Technology_UNKNOWN_TECHNOLOGY: fallthrough default: diff --git a/cmd/daemon/vpn_no_quench.go b/cmd/daemon/vpn_no_quench.go index fb2d556f..eaa06275 100644 --- a/cmd/daemon/vpn_no_quench.go +++ b/cmd/daemon/vpn_no_quench.go @@ -2,8 +2,12 @@ package main -import "github.com/NordSecurity/nordvpn-linux/daemon/vpn" +import ( + "fmt" -func getQuenchVPN(fwmark uint32) vpn.VPN { - return nil + "github.com/NordSecurity/nordvpn-linux/daemon/vpn" +) + +func getQuenchVPN(fwmark uint32) (vpn.VPN, error) { + return nil, fmt.Errorf("quench is not enabled") } diff --git a/cmd/daemon/vpn_quench.go b/cmd/daemon/vpn_quench.go index ab65b5e2..0ee7aabb 100644 --- a/cmd/daemon/vpn_quench.go +++ b/cmd/daemon/vpn_quench.go @@ -4,6 +4,6 @@ package main import "github.com/NordSecurity/nordvpn-linux/daemon/vpn/quench" -func getQuenchVPN(fwmark uint32) *quench.Quench { - return quench.New(fwmark) +func getQuenchVPN(fwmark uint32) (*quench.Quench, error) { + return quench.New(fwmark), nil } diff --git a/cmd/daemon/vpnconfig_linux.go b/cmd/daemon/vpnconfig_linux.go index bccf49a5..cb339cce 100644 --- a/cmd/daemon/vpnconfig_linux.go +++ b/cmd/daemon/vpnconfig_linux.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/NordSecurity/nordvpn-linux/config" + "github.com/NordSecurity/nordvpn-linux/config/remote" "github.com/NordSecurity/nordvpn-linux/daemon/vpn" ) @@ -15,6 +16,6 @@ func (noopConfigGetter) GetConfig(string) (string, error) { return "", fmt.Errorf("config is not available") } -func vpnLibConfigGetterImplementation(_ config.Manager) vpn.LibConfigGetter { +func vpnLibConfigGetterImplementation(_ config.Manager, _ remote.RemoteConfigGetter) vpn.LibConfigGetter { return noopConfigGetter{} } diff --git a/cmd/daemon/vpnconfig_telio.go b/cmd/daemon/vpnconfig_telio.go index c571c05c..ea2f9651 100644 --- a/cmd/daemon/vpnconfig_telio.go +++ b/cmd/daemon/vpnconfig_telio.go @@ -9,7 +9,6 @@ import ( "github.com/NordSecurity/nordvpn-linux/daemon/vpn/nordlynx/libtelio" ) -func vpnLibConfigGetterImplementation(cm config.Manager) vpn.LibConfigGetter { - rcConfig := remote.NewRConfig(remote.UpdatePeriod, remote.NewFirebaseService(FirebaseToken), cm) +func vpnLibConfigGetterImplementation(cm config.Manager, rcConfig remote.RemoteConfigGetter) vpn.LibConfigGetter { return libtelio.NewTelioConfig(rcConfig) } diff --git a/config/remote/firebase.go b/config/remote/firebase.go index 01f8980a..bad41c85 100644 --- a/config/remote/firebase.go +++ b/config/remote/firebase.go @@ -8,7 +8,9 @@ import ( "log" "net/http" "sort" + "strconv" "strings" + "sync" "time" "github.com/NordSecurity/nordvpn-linux/config" @@ -44,6 +46,7 @@ type ServiceAccount struct { } type RConfig struct { + mu sync.Mutex updatePeriod time.Duration config *firebaseremoteconfig.RemoteConfig remoteService RemoteConfigService @@ -122,7 +125,7 @@ func (rc *RConfig) fetchAndSaveRemoteConfig() (remoteConfig []byte, err error) { } // GetValue provides value of requested key from remote config -func (rc *RConfig) GetValue(cfgKey string) (string, error) { +func (rc *RConfig) getValue(cfgKey string) (string, error) { err := rc.fetchRemoteConfigIfTime() if err != nil { log.Println(internal.WarningPrefix, "using cached config:", err) @@ -159,9 +162,37 @@ func stringToSemVersion(stringVersion, prefix string) (*semver.Version, error) { return semver.NewVersion(stringVersion) } +func (rc *RConfig) GetQuenchEnabled(stringVersion string) (bool, error) { + rc.mu.Lock() + defer rc.mu.Unlock() + + enabledStr, err := rc.getRemoteConfigByVersion(RcQuenchConfigFieldPrefix, stringVersion) + if err != nil { + return false, fmt.Errorf("fetching the telio config: %w", err) + } + + enabled, err := strconv.ParseBool(enabledStr) + if err != nil { + return false, fmt.Errorf("parsing firebase parameter: %w", err) + } + + return enabled, nil +} + // GetTelioConfig try to find remote config field for app version // and load json block from that field func (rc *RConfig) GetTelioConfig(stringVersion string) (string, error) { + rc.mu.Lock() + defer rc.mu.Unlock() + + cfg, err := rc.getRemoteConfigByVersion(RcTelioConfigFieldPrefix, stringVersion) + if err != nil { + return "", fmt.Errorf("fetching the telio config: %w", err) + } + return cfg, nil +} + +func (rc *RConfig) getRemoteConfigByVersion(prefix string, stringVersion string) (string, error) { if err := rc.fetchRemoteConfigIfTime(); err != nil { if len(rc.config.Parameters) == 0 { return "", err @@ -177,8 +208,8 @@ func (rc *RConfig) GetTelioConfig(stringVersion string) (string, error) { // build descending ordered version list orderedFields := []*fieldVersion{} for key := range rc.config.Parameters { - if strings.HasPrefix(key, RcTelioConfigFieldPrefix) { - ver, err := stringToSemVersion(key, RcTelioConfigFieldPrefix) + if strings.HasPrefix(key, prefix) { + ver, err := stringToSemVersion(key, prefix) if err != nil { log.Println(err) continue @@ -194,7 +225,7 @@ func (rc *RConfig) GetTelioConfig(stringVersion string) (string, error) { } log.Println("remote config version field:", versionField) - jsonString, err := rc.GetValue(versionField) + jsonString, err := rc.getValue(versionField) if err != nil { return "", err } diff --git a/config/remote/firebase_test.go b/config/remote/firebase_test.go index 1bed66ab..bb773d6a 100644 --- a/config/remote/firebase_test.go +++ b/config/remote/firebase_test.go @@ -38,7 +38,7 @@ func TestRemoteConfig_GetValue(t *testing.T) { category.Set(t, category.File) initConfig() rc := NewRConfig(time.Duration(0), &remoteServiceMock{}, getConfigManager()) - welcomeMessage, err := rc.GetValue("welcome_message") + welcomeMessage, err := rc.getValue("welcome_message") assert.NoError(t, err) assert.Equal(t, "hola", welcomeMessage) } @@ -48,10 +48,10 @@ func TestRemoteConfig_Caching(t *testing.T) { initConfig() rsm := remoteServiceMock{} rc := NewRConfig(time.Hour*24, &rsm, getConfigManager()) - _, err := rc.GetValue("welcome_message") + _, err := rc.getValue("welcome_message") assert.NoError(t, err) assert.Equal(t, 1, rsm.fetchCount) - welcomeMessage, err := rc.GetValue("welcome_message") + welcomeMessage, err := rc.getValue("welcome_message") assert.NoError(t, err) assert.Equal(t, "hola", welcomeMessage) assert.Equal(t, 1, rsm.fetchCount) @@ -62,10 +62,10 @@ func TestRemoteConfig_NoCaching(t *testing.T) { initConfig() rsm := remoteServiceMock{} rc := NewRConfig(0, &rsm, getConfigManager()) - _, err := rc.GetValue("welcome_message") + _, err := rc.getValue("welcome_message") assert.NoError(t, err) assert.Equal(t, 1, rsm.fetchCount) - welcomeMessage, err := rc.GetValue("welcome_message") + welcomeMessage, err := rc.getValue("welcome_message") assert.NoError(t, err) assert.Equal(t, "hola", welcomeMessage) assert.Equal(t, 2, rsm.fetchCount) @@ -361,7 +361,7 @@ func TestRemoteConfig_GetCachedData(t *testing.T) { cm.Cfg.RCLastUpdate = time.Now() cm.Cfg.RemoteConfig = test.cachedValue - value, err := rc.GetValue("welcome_message") + value, err := rc.getValue("welcome_message") assert.Equal(t, test.expectedValue, value) if test.expectedValue == "" { diff --git a/config/remote/remote_config.go b/config/remote/remote_config.go index a9d40983..386bf257 100644 --- a/config/remote/remote_config.go +++ b/config/remote/remote_config.go @@ -15,11 +15,12 @@ const ( // and digits, underscores) app will try to find field corresponding to // app's version, but if exact match is not found then first older version // is chosen, if that one is also not available, then use local defaults. - RcTelioConfigFieldPrefix = "telio_config_" + RcTelioConfigFieldPrefix = "telio_config_" + RcQuenchConfigFieldPrefix = "quench_enabled_" ) // RemoteConfigGetter get values from remote config type RemoteConfigGetter interface { - GetValue(key string) (string, error) GetTelioConfig(version string) (string, error) + GetQuenchEnabled(version string) (bool, error) } diff --git a/daemon/pb/quench_hidden.pb.go b/daemon/pb/quench_hidden.pb.go new file mode 100644 index 00000000..3d446e2e --- /dev/null +++ b/daemon/pb/quench_hidden.pb.go @@ -0,0 +1,128 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.1 +// protoc v3.21.6 +// source: quench_hidden.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type QuenchEnabled struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` +} + +func (x *QuenchEnabled) Reset() { + *x = QuenchEnabled{} + mi := &file_quench_hidden_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QuenchEnabled) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QuenchEnabled) ProtoMessage() {} + +func (x *QuenchEnabled) ProtoReflect() protoreflect.Message { + mi := &file_quench_hidden_proto_msgTypes[0] + 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 QuenchEnabled.ProtoReflect.Descriptor instead. +func (*QuenchEnabled) Descriptor() ([]byte, []int) { + return file_quench_hidden_proto_rawDescGZIP(), []int{0} +} + +func (x *QuenchEnabled) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +var File_quench_hidden_proto protoreflect.FileDescriptor + +var file_quench_hidden_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x68, 0x5f, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62, 0x22, 0x29, 0x0a, 0x0d, 0x51, 0x75, 0x65, + 0x6e, 0x63, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x4e, 0x6f, 0x72, 0x64, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, + 0x6e, 0x6f, 0x72, 0x64, 0x76, 0x70, 0x6e, 0x2d, 0x6c, 0x69, 0x6e, 0x75, 0x78, 0x2f, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_quench_hidden_proto_rawDescOnce sync.Once + file_quench_hidden_proto_rawDescData = file_quench_hidden_proto_rawDesc +) + +func file_quench_hidden_proto_rawDescGZIP() []byte { + file_quench_hidden_proto_rawDescOnce.Do(func() { + file_quench_hidden_proto_rawDescData = protoimpl.X.CompressGZIP(file_quench_hidden_proto_rawDescData) + }) + return file_quench_hidden_proto_rawDescData +} + +var file_quench_hidden_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_quench_hidden_proto_goTypes = []any{ + (*QuenchEnabled)(nil), // 0: pb.QuenchEnabled +} +var file_quench_hidden_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_quench_hidden_proto_init() } +func file_quench_hidden_proto_init() { + if File_quench_hidden_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_quench_hidden_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_quench_hidden_proto_goTypes, + DependencyIndexes: file_quench_hidden_proto_depIdxs, + MessageInfos: file_quench_hidden_proto_msgTypes, + }.Build() + File_quench_hidden_proto = out.File + file_quench_hidden_proto_rawDesc = nil + file_quench_hidden_proto_goTypes = nil + file_quench_hidden_proto_depIdxs = nil +} diff --git a/daemon/pb/service_grpc.pb.go b/daemon/pb/service_grpc.pb.go index 86d0b0ed..08b43ed5 100644 --- a/daemon/pb/service_grpc.pb.go +++ b/daemon/pb/service_grpc.pb.go @@ -64,6 +64,7 @@ const ( Daemon_SubscribeToStateChanges_FullMethodName = "/pb.Daemon/SubscribeToStateChanges" Daemon_GetServers_FullMethodName = "/pb.Daemon/GetServers" Daemon_SetPostQuantum_FullMethodName = "/pb.Daemon/SetPostQuantum" + Daemon_IsQuenchEnabled_FullMethodName = "/pb.Daemon/IsQuenchEnabled" ) // DaemonClient is the client API for Daemon service. @@ -115,6 +116,7 @@ type DaemonClient interface { SubscribeToStateChanges(ctx context.Context, in *Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[AppState], error) GetServers(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*ServersResponse, error) SetPostQuantum(ctx context.Context, in *SetGenericRequest, opts ...grpc.CallOption) (*Payload, error) + IsQuenchEnabled(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*QuenchEnabled, error) } type daemonClient struct { @@ -611,6 +613,16 @@ func (c *daemonClient) SetPostQuantum(ctx context.Context, in *SetGenericRequest return out, nil } +func (c *daemonClient) IsQuenchEnabled(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*QuenchEnabled, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(QuenchEnabled) + err := c.cc.Invoke(ctx, Daemon_IsQuenchEnabled_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // DaemonServer is the server API for Daemon service. // All implementations must embed UnimplementedDaemonServer // for forward compatibility. @@ -660,6 +672,7 @@ type DaemonServer interface { SubscribeToStateChanges(*Empty, grpc.ServerStreamingServer[AppState]) error GetServers(context.Context, *Empty) (*ServersResponse, error) SetPostQuantum(context.Context, *SetGenericRequest) (*Payload, error) + IsQuenchEnabled(context.Context, *Empty) (*QuenchEnabled, error) mustEmbedUnimplementedDaemonServer() } @@ -805,6 +818,9 @@ func (UnimplementedDaemonServer) GetServers(context.Context, *Empty) (*ServersRe func (UnimplementedDaemonServer) SetPostQuantum(context.Context, *SetGenericRequest) (*Payload, error) { return nil, status.Errorf(codes.Unimplemented, "method SetPostQuantum not implemented") } +func (UnimplementedDaemonServer) IsQuenchEnabled(context.Context, *Empty) (*QuenchEnabled, error) { + return nil, status.Errorf(codes.Unimplemented, "method IsQuenchEnabled not implemented") +} func (UnimplementedDaemonServer) mustEmbedUnimplementedDaemonServer() {} func (UnimplementedDaemonServer) testEmbeddedByValue() {} @@ -1608,6 +1624,24 @@ func _Daemon_SetPostQuantum_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _Daemon_IsQuenchEnabled_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServer).IsQuenchEnabled(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Daemon_IsQuenchEnabled_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServer).IsQuenchEnabled(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + // Daemon_ServiceDesc is the grpc.ServiceDesc for Daemon service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1779,6 +1813,10 @@ var Daemon_ServiceDesc = grpc.ServiceDesc{ MethodName: "SetPostQuantum", Handler: _Daemon_SetPostQuantum_Handler, }, + { + MethodName: "IsQuenchEnabled", + Handler: _Daemon_IsQuenchEnabled_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/daemon/rpc.go b/daemon/rpc.go index e864fb3f..f3c6bcea 100644 --- a/daemon/rpc.go +++ b/daemon/rpc.go @@ -8,6 +8,7 @@ import ( "github.com/NordSecurity/nordvpn-linux/auth" "github.com/NordSecurity/nordvpn-linux/config" + "github.com/NordSecurity/nordvpn-linux/config/remote" "github.com/NordSecurity/nordvpn-linux/core" "github.com/NordSecurity/nordvpn-linux/core/mesh" "github.com/NordSecurity/nordvpn-linux/daemon/dns" @@ -56,6 +57,7 @@ type RPC struct { statePublisher *state.StatePublisher ConnectionParameters ParametersStorage connectContext *sharedctx.Context + remoteConfigGetter remote.RemoteConfigGetter pb.UnimplementedDaemonServer } @@ -83,32 +85,34 @@ func NewRPC( meshRegistry mesh.Registry, statePublisher *state.StatePublisher, connectContext *sharedctx.Context, + remoteConfigGetter remote.RemoteConfigGetter, ) *RPC { scheduler, _ := gocron.NewScheduler(gocron.WithLocation(time.UTC)) return &RPC{ - environment: environment, - ac: ac, - cm: cm, - dm: dm, - api: api, - serversAPI: serversAPI, - credentialsAPI: credentialsAPI, - cdn: cdn, - repo: repo, - authentication: authentication, - version: version, - factory: factory, - events: events, - endpointResolver: endpointResolver, - scheduler: scheduler, - netw: netw, - publisher: publisher, - nameservers: nameservers, - ncClient: ncClient, - analytics: analytics, - norduser: norduser, - meshRegistry: meshRegistry, - statePublisher: statePublisher, - connectContext: connectContext, + environment: environment, + ac: ac, + cm: cm, + dm: dm, + api: api, + serversAPI: serversAPI, + credentialsAPI: credentialsAPI, + cdn: cdn, + repo: repo, + authentication: authentication, + version: version, + factory: factory, + events: events, + endpointResolver: endpointResolver, + scheduler: scheduler, + netw: netw, + publisher: publisher, + nameservers: nameservers, + ncClient: ncClient, + analytics: analytics, + norduser: norduser, + meshRegistry: meshRegistry, + statePublisher: statePublisher, + connectContext: connectContext, + remoteConfigGetter: remoteConfigGetter, } } diff --git a/daemon/rpc_connect.go b/daemon/rpc_connect.go index 18993764..502e5817 100644 --- a/daemon/rpc_connect.go +++ b/daemon/rpc_connect.go @@ -12,6 +12,7 @@ import ( "github.com/NordSecurity/nordvpn-linux/daemon/pb" "github.com/NordSecurity/nordvpn-linux/daemon/vpn" "github.com/NordSecurity/nordvpn-linux/events" + "github.com/NordSecurity/nordvpn-linux/features" "github.com/NordSecurity/nordvpn-linux/internal" "github.com/NordSecurity/nordvpn-linux/network" ) @@ -44,6 +45,34 @@ func (r *RPC) Connect(in *pb.ConnectRequest, srv pb.Daemon_ConnectServer) (retEr return err } +// quenchConfigFallback checks if technology is configured to quench and falls back to NordLynx if quench was disabled +// in compile time or in remote config. It returns a new config with desired technology set. +func (r *RPC) quenchConfigFallback(cfg config.Config) config.Config { + if cfg.Technology != config.Technology_QUENCH { + return cfg + } + + quenchEnabled, err := r.remoteConfigGetter.GetQuenchEnabled(r.version) + if err != nil { + log.Println(internal.ErrorPrefix, "failed to retrieve remote config for quench:", err) + } + + if features.QuenchEnabled && (quenchEnabled && err == nil) { + return cfg + } + + log.Println(internal.DebugPrefix, + "user had configured Quench technolgy, but it was disabled, falling back to NordLynx") + + cfg.Technology = config.Technology_QUENCH + r.cm.SaveWith(func(c config.Config) config.Config { + c.Technology = config.Technology_QUENCH + return c + }) + + return cfg +} + func (r *RPC) connect( ctx context.Context, in *pb.ConnectRequest, @@ -65,6 +94,7 @@ func (r *RPC) connect( if err := r.cm.Load(&cfg); err != nil { log.Println(internal.ErrorPrefix, err) } + cfg = r.quenchConfigFallback(cfg) insights := r.dm.GetInsightsData().Insights diff --git a/daemon/rpc_is_quench_enabled.go b/daemon/rpc_is_quench_enabled.go new file mode 100644 index 00000000..8f9f6992 --- /dev/null +++ b/daemon/rpc_is_quench_enabled.go @@ -0,0 +1,23 @@ +package daemon + +import ( + "context" + "log" + + "github.com/NordSecurity/nordvpn-linux/daemon/pb" + "github.com/NordSecurity/nordvpn-linux/features" + "github.com/NordSecurity/nordvpn-linux/internal" +) + +func (r *RPC) IsQuenchEnabled(ctx context.Context, in *pb.Empty) (*pb.QuenchEnabled, error) { + if !features.QuenchEnabled { + return &pb.QuenchEnabled{Enabled: false}, nil + } + quenchEnabled, err := r.remoteConfigGetter.GetQuenchEnabled(r.version) + if err != nil { + log.Println(internal.ErrorPrefix, "failed to determine if quench is enabled based on firebase config:", err) + return &pb.QuenchEnabled{Enabled: false}, nil + } + + return &pb.QuenchEnabled{Enabled: quenchEnabled}, nil +} diff --git a/daemon/rpc_is_quench_enabled_test.go b/daemon/rpc_is_quench_enabled_test.go new file mode 100644 index 00000000..0e82ea83 --- /dev/null +++ b/daemon/rpc_is_quench_enabled_test.go @@ -0,0 +1,42 @@ +package daemon + +import ( + "context" + "fmt" + "testing" + + "github.com/NordSecurity/nordvpn-linux/daemon/pb" + "github.com/NordSecurity/nordvpn-linux/test/category" + "github.com/NordSecurity/nordvpn-linux/test/mock" + "github.com/stretchr/testify/assert" +) + +func TestIsQuenchEnabled(t *testing.T) { + category.Set(t, category.Unit) + + remoteConfigGetter := mock.NewRemoteConfigMock() + r := RPC{ + remoteConfigGetter: remoteConfigGetter, + } + + tests := []struct { + name string + quenchEnabledErr error + }{ + { + name: "quench disabled", + }, + { + name: "failed to get quench status", + quenchEnabledErr: fmt.Errorf("failed to get quench status"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + resp, err := r.IsQuenchEnabled(context.Background(), &pb.Empty{}) + assert.Nil(t, err, "Unexpected error returned by IsQuenchEnabled rpc.") + assert.Equal(t, resp.Enabled, false, "Unexpected response type received.") + }) + } +} diff --git a/daemon/rpc_set_technology.go b/daemon/rpc_set_technology.go index 67af3c97..695105d7 100644 --- a/daemon/rpc_set_technology.go +++ b/daemon/rpc_set_technology.go @@ -7,10 +7,36 @@ import ( "github.com/NordSecurity/nordvpn-linux/config" "github.com/NordSecurity/nordvpn-linux/daemon/pb" + "github.com/NordSecurity/nordvpn-linux/features" "github.com/NordSecurity/nordvpn-linux/internal" ) func (r *RPC) SetTechnology(ctx context.Context, in *pb.SetTechnologyRequest) (*pb.Payload, error) { + if in.Technology == config.Technology_QUENCH { + if !features.QuenchEnabled { + log.Println(internal.DebugPrefix, + "user requested a quench technology but the feature is hidden based on compile flag.") + return &pb.Payload{ + Type: internal.CodeFeatureHidden, + }, nil + } + quenchEnabled, err := r.remoteConfigGetter.GetQuenchEnabled(r.version) + if err != nil { + log.Println(internal.ErrorPrefix, "failed to determine if quench is enabled by remote config:", err) + return &pb.Payload{ + Type: internal.CodeFeatureHidden, + }, nil + } + + if !quenchEnabled { + log.Println(internal.ErrorPrefix, + "user rquested a quench technology but the feature is hidden based on remote config flag") + return &pb.Payload{ + Type: internal.CodeFeatureHidden, + }, nil + } + } + var cfg config.Config if err := r.cm.Load(&cfg); err != nil { log.Println(internal.ErrorPrefix, err) diff --git a/daemon/rpc_set_technology_test.go b/daemon/rpc_set_technology_test.go new file mode 100644 index 00000000..427510c7 --- /dev/null +++ b/daemon/rpc_set_technology_test.go @@ -0,0 +1,45 @@ +package daemon + +import ( + "context" + "fmt" + "testing" + + "github.com/NordSecurity/nordvpn-linux/config" + "github.com/NordSecurity/nordvpn-linux/daemon/pb" + "github.com/NordSecurity/nordvpn-linux/internal" + "github.com/NordSecurity/nordvpn-linux/test/category" + "github.com/NordSecurity/nordvpn-linux/test/mock" + "github.com/stretchr/testify/assert" +) + +func TestSetTechnology_Quench(t *testing.T) { + category.Set(t, category.Unit) + + remoteConfigGetter := mock.NewRemoteConfigMock() + r := RPC{ + remoteConfigGetter: remoteConfigGetter, + } + + tests := []struct { + name string + quenchEnabledErr error + }{ + { + name: "quench disabled", + }, + { + name: "failed to get quench status", + quenchEnabledErr: fmt.Errorf("failed to get quench status"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + resp, err := r.SetTechnology(context.Background(), + &pb.SetTechnologyRequest{Technology: config.Technology_QUENCH}) + assert.Nil(t, err, "Unexpected error returned by IsQuenchEnabled rpc.") + assert.Equal(t, resp.Type, internal.CodeFeatureHidden, "Unexpected response type received.") + }) + } +} diff --git a/daemon/rpc_test.go b/daemon/rpc_test.go index e27e55d5..e550eb7f 100644 --- a/daemon/rpc_test.go +++ b/daemon/rpc_test.go @@ -99,6 +99,7 @@ func testRPC() *RPC { &RegistryMock{}, nil, sharedctx.New(), + mock.NewRemoteConfigMock(), ) } diff --git a/features/no_quench.go b/features/no_quench.go new file mode 100644 index 00000000..19718805 --- /dev/null +++ b/features/no_quench.go @@ -0,0 +1,5 @@ +//go:build !quench + +package features + +const QuenchEnabled = false diff --git a/features/quench.go b/features/quench.go new file mode 100644 index 00000000..ff142d2f --- /dev/null +++ b/features/quench.go @@ -0,0 +1,5 @@ +//go:build quench + +package features + +const QuenchEnabled = true diff --git a/internal/codes.go b/internal/codes.go index 2a2074c7..4a3198b3 100644 --- a/internal/codes.go +++ b/internal/codes.go @@ -61,6 +61,7 @@ const ( CodeAllowlistPortNoop int64 = 3047 CodePqAndMeshnetSimultaneously int64 = 3048 CodePqWithoutNordlynx int64 = 3049 + CodeFeatureHidden int64 = 3050 ) type ErrorWithCode struct { diff --git a/protobuf/daemon/quench_hidden.proto b/protobuf/daemon/quench_hidden.proto new file mode 100644 index 00000000..b28ffb20 --- /dev/null +++ b/protobuf/daemon/quench_hidden.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package pb; + +option go_package = "github.com/NordSecurity/nordvpn-linux/daemon/pb"; + +message QuenchEnabled { + bool enabled = 1; +} diff --git a/protobuf/daemon/service.proto b/protobuf/daemon/service.proto index 362780bd..96055e7b 100644 --- a/protobuf/daemon/service.proto +++ b/protobuf/daemon/service.proto @@ -22,6 +22,7 @@ import "token.proto"; import "purchase.proto"; import "state.proto"; import "servers.proto"; +import "quench_hidden.proto"; service Daemon { rpc AccountInfo(Empty) returns (AccountResponse); @@ -69,4 +70,5 @@ service Daemon { rpc SubscribeToStateChanges(Empty) returns (stream AppState); rpc GetServers(Empty) returns (ServersResponse); rpc SetPostQuantum(SetGenericRequest) returns (Payload); + rpc IsQuenchEnabled(Empty) returns (QuenchEnabled); } diff --git a/test/mock/remote_config.go b/test/mock/remote_config.go new file mode 100644 index 00000000..ebc913d0 --- /dev/null +++ b/test/mock/remote_config.go @@ -0,0 +1,18 @@ +package mock + +type RemoteConfigMock struct { + QuenchEnabled bool + GetQuenchErr error +} + +func NewRemoteConfigMock() *RemoteConfigMock { + return &RemoteConfigMock{} +} + +func (r *RemoteConfigMock) GetTelioConfig(version string) (string, error) { + return "", nil +} + +func (r *RemoteConfigMock) GetQuenchEnabled(version string) (bool, error) { + return r.QuenchEnabled, r.GetQuenchErr +}