Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client/android/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string
return profilemanager.WriteOutConfig(a.cfgPath, a.config)
}

// LoginWithSetupKeySync performs a synchronous setup key login and saves the config.
// This is used by the MDM managed configuration flow where the native app controls threading.
func (a *Auth) LoginWithSetupKeySync(setupKey string, deviceName string) error {
return a.loginWithSetupKeyAndSaveConfig(setupKey, deviceName)
}

// Login try register the client on the server
func (a *Auth) Login(resultListener ErrListener, urlOpener URLOpener, isAndroidTV bool) {
go func() {
Expand Down
174 changes: 174 additions & 0 deletions client/android/managed_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package android

import (
"fmt"

log "github.com/sirupsen/logrus"

"github.com/netbirdio/netbird/client/internal/profilemanager"
)

// Managed configuration key names for Android Enterprise managed configurations.
// These match the keys defined in app_restrictions.xml on the Android native app side.
const (
managedConfigKeyManagementURL = "managementUrl"
managedConfigKeySetupKey = "setupKey"
managedConfigKeyAdminURL = "adminUrl"
managedConfigKeyPreSharedKey = "preSharedKey"
managedConfigKeyRosenpassEnabled = "rosenpassEnabled"
managedConfigKeyRosenpassPerm = "rosenpassPermissive"
managedConfigKeyDisableAutoConn = "disableAutoConnect"
)

// Key name getters for the Android Java client to reference the same key constants.

// GetManagedConfigKeyManagementURL returns the key name for management URL
func GetManagedConfigKeyManagementURL() string { return managedConfigKeyManagementURL }

// GetManagedConfigKeySetupKey returns the key name for setup key
func GetManagedConfigKeySetupKey() string { return managedConfigKeySetupKey }

// GetManagedConfigKeyAdminURL returns the key name for admin URL
func GetManagedConfigKeyAdminURL() string { return managedConfigKeyAdminURL }

// GetManagedConfigKeyPreSharedKey returns the key name for pre-shared key
func GetManagedConfigKeyPreSharedKey() string { return managedConfigKeyPreSharedKey }

// GetManagedConfigKeyRosenpassEnabled returns the key name for Rosenpass enabled
func GetManagedConfigKeyRosenpassEnabled() string { return managedConfigKeyRosenpassEnabled }

// GetManagedConfigKeyRosenpassPermissive returns the key name for Rosenpass permissive
func GetManagedConfigKeyRosenpassPermissive() string { return managedConfigKeyRosenpassPerm }

// GetManagedConfigKeyDisableAutoConnect returns the key name for disable auto-connect
func GetManagedConfigKeyDisableAutoConnect() string { return managedConfigKeyDisableAutoConn }

// ManagedConfig holds configuration values pushed by an MDM/EMM via Android Enterprise
// managed configurations (app restrictions). Values set here override user preferences
// on every app launch.
//
// The native Android app reads from RestrictionsManager and populates this struct
// via the setter methods, then calls Apply() to write the values to the config file.
type ManagedConfig struct {
managementURL string
setupKey string
adminURL string
preSharedKey *string
rosenpassEnabled *bool
rosenpassPerm *bool
disableAutoConn *bool
}

// NewManagedConfig creates a new empty ManagedConfig
func NewManagedConfig() *ManagedConfig {
return &ManagedConfig{}
}

// SetManagementURL sets the management server URL from MDM config
func (m *ManagedConfig) SetManagementURL(url string) {
m.managementURL = url
}

// SetSetupKey sets the setup key for silent device registration from MDM config
func (m *ManagedConfig) SetSetupKey(key string) {
m.setupKey = key
}

// SetAdminURL sets the admin dashboard URL from MDM config
func (m *ManagedConfig) SetAdminURL(url string) {
m.adminURL = url
}

// SetPreSharedKey sets the WireGuard pre-shared key from MDM config
func (m *ManagedConfig) SetPreSharedKey(key string) {
m.preSharedKey = &key
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// SetRosenpassEnabled sets whether Rosenpass post-quantum encryption is enabled
func (m *ManagedConfig) SetRosenpassEnabled(enabled bool) {
m.rosenpassEnabled = &enabled
}

// SetRosenpassPermissive sets whether Rosenpass permissive mode is enabled
func (m *ManagedConfig) SetRosenpassPermissive(permissive bool) {
m.rosenpassPerm = &permissive
}

// SetDisableAutoConnect sets whether auto-connect on launch is disabled
func (m *ManagedConfig) SetDisableAutoConnect(disable bool) {
m.disableAutoConn = &disable
}

// HasSetupKey returns true if a setup key was provided by MDM
func (m *ManagedConfig) HasSetupKey() bool {
return m.setupKey != ""
}

// GetSetupKey returns the MDM-provided setup key
func (m *ManagedConfig) GetSetupKey() string {
return m.setupKey
}

// HasConfig returns true if any configuration value was set by MDM
func (m *ManagedConfig) HasConfig() bool {
return m.managementURL != "" ||
m.setupKey != "" ||
m.adminURL != "" ||
m.preSharedKey != nil ||
m.rosenpassEnabled != nil ||
m.rosenpassPerm != nil ||
m.disableAutoConn != nil
}

// Apply writes the MDM-managed configuration values to the config file at configPath.
// Values provided by MDM override any existing user-set values.
// The setup key is NOT written to the config file — it is used separately for registration.
func (m *ManagedConfig) Apply(configPath string) error {
if !m.HasConfig() {
return nil
}

log.Info("Applying MDM managed configuration")

Comment thread
coderabbitai[bot] marked this conversation as resolved.
input := profilemanager.ConfigInput{
ConfigPath: configPath,
}

if m.managementURL != "" {
input.ManagementURL = m.managementURL
log.Infof("MDM: setting management URL")
}

if m.adminURL != "" {
input.AdminURL = m.adminURL
log.Infof("MDM: setting admin URL")
}

if m.preSharedKey != nil {
input.PreSharedKey = m.preSharedKey
log.Infof("MDM: setting pre-shared key")
}

if m.rosenpassEnabled != nil {
input.RosenpassEnabled = m.rosenpassEnabled
log.Infof("MDM: setting Rosenpass enabled=%v", *m.rosenpassEnabled)
}

if m.rosenpassPerm != nil {
input.RosenpassPermissive = m.rosenpassPerm
log.Infof("MDM: setting Rosenpass permissive=%v", *m.rosenpassPerm)
}

if m.disableAutoConn != nil {
input.DisableAutoConnect = m.disableAutoConn
log.Infof("MDM: setting disable auto-connect=%v", *m.disableAutoConn)
}

_, err := profilemanager.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to apply MDM config: %w", err)
}

log.Info("MDM managed configuration applied successfully")
return nil
}
181 changes: 181 additions & 0 deletions client/android/managed_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package android

import (
"path/filepath"
"testing"

"github.com/netbirdio/netbird/client/internal/profilemanager"
)

func TestManagedConfig_NewIsEmpty(t *testing.T) {
m := NewManagedConfig()
if m.HasConfig() {
t.Error("new ManagedConfig should not have config")
}
if m.HasSetupKey() {
t.Error("new ManagedConfig should not have setup key")
}
if m.GetSetupKey() != "" {
t.Error("new ManagedConfig setup key should be empty")
}
}

func TestManagedConfig_SettersMarkHasConfig(t *testing.T) {
tests := []struct {
name string
setter func(*ManagedConfig)
}{
{"managementURL", func(m *ManagedConfig) { m.SetManagementURL("https://example.com") }},
{"setupKey", func(m *ManagedConfig) { m.SetSetupKey("test-key") }},
{"adminURL", func(m *ManagedConfig) { m.SetAdminURL("https://admin.example.com") }},
{"preSharedKey", func(m *ManagedConfig) { m.SetPreSharedKey("psk123") }},
{"rosenpassEnabled", func(m *ManagedConfig) { m.SetRosenpassEnabled(true) }},
{"rosenpassPermissive", func(m *ManagedConfig) { m.SetRosenpassPermissive(true) }},
{"disableAutoConnect", func(m *ManagedConfig) { m.SetDisableAutoConnect(true) }},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewManagedConfig()
tt.setter(m)
if !m.HasConfig() {
t.Errorf("HasConfig() should be true after setting %s", tt.name)
}
})
}
}

func TestManagedConfig_SetupKey(t *testing.T) {
m := NewManagedConfig()
m.SetSetupKey("my-setup-key")
if !m.HasSetupKey() {
t.Error("HasSetupKey() should be true")
}
if m.GetSetupKey() != "my-setup-key" {
t.Errorf("GetSetupKey() = %q, want %q", m.GetSetupKey(), "my-setup-key")
}
}

func TestManagedConfig_ApplyEmpty(t *testing.T) {
m := NewManagedConfig()
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
err := m.Apply(cfgFile)
if err != nil {
t.Fatalf("Apply on empty config should not error: %v", err)
}
}

func TestManagedConfig_ApplyManagementURL(t *testing.T) {
cfgFile := filepath.Join(t.TempDir(), "netbird.json")

m := NewManagedConfig()
m.SetManagementURL("https://custom.mgmt.example.com:443")
err := m.Apply(cfgFile)
if err != nil {
t.Fatalf("Apply failed: %v", err)
}

cfg, err := profilemanager.ReadConfig(cfgFile)
if err != nil {
t.Fatalf("ReadConfig failed: %v", err)
}
if cfg.ManagementURL.String() != "https://custom.mgmt.example.com:443" {
t.Errorf("ManagementURL = %q, want %q", cfg.ManagementURL.String(), "https://custom.mgmt.example.com:443")
}
}

func TestManagedConfig_ApplyOverridesExisting(t *testing.T) {
cfgFile := filepath.Join(t.TempDir(), "netbird.json")

// Create initial config with default URL
p := NewPreferences(cfgFile)
p.SetManagementURL("https://original.example.com:443")
if err := p.Commit(); err != nil {
t.Fatalf("initial Commit failed: %v", err)
}

// Apply MDM config that overrides the URL
m := NewManagedConfig()
m.SetManagementURL("https://mdm-managed.example.com:443")
m.SetRosenpassEnabled(true)
err := m.Apply(cfgFile)
if err != nil {
t.Fatalf("Apply failed: %v", err)
}

cfg, err := profilemanager.ReadConfig(cfgFile)
if err != nil {
t.Fatalf("ReadConfig failed: %v", err)
}
if cfg.ManagementURL.String() != "https://mdm-managed.example.com:443" {
t.Errorf("ManagementURL = %q, want %q", cfg.ManagementURL.String(), "https://mdm-managed.example.com:443")
}
if !cfg.RosenpassEnabled {
t.Error("RosenpassEnabled should be true after MDM apply")
}
}

func TestManagedConfig_ApplyPreSharedKey(t *testing.T) {
cfgFile := filepath.Join(t.TempDir(), "netbird.json")

m := NewManagedConfig()
m.SetPreSharedKey("mdm-psk-value")
err := m.Apply(cfgFile)
if err != nil {
t.Fatalf("Apply failed: %v", err)
}

cfg, err := profilemanager.ReadConfig(cfgFile)
if err != nil {
t.Fatalf("ReadConfig failed: %v", err)
}
if cfg.PreSharedKey != "mdm-psk-value" {
t.Errorf("PreSharedKey = %q, want %q", cfg.PreSharedKey, "mdm-psk-value")
}
}

func TestManagedConfig_SetupKeyNotWrittenToConfig(t *testing.T) {
cfgFile := filepath.Join(t.TempDir(), "netbird.json")

m := NewManagedConfig()
m.SetSetupKey("secret-setup-key")
m.SetManagementURL("https://example.com:443")
err := m.Apply(cfgFile)
if err != nil {
t.Fatalf("Apply failed: %v", err)
}

// The setup key should NOT be in the config file — it's only used for registration
cfg, err := profilemanager.ReadConfig(cfgFile)
if err != nil {
t.Fatalf("ReadConfig failed: %v", err)
}
// Config has no SetupKey field, so if we got here without error, the key was correctly not written
if cfg.ManagementURL.String() != "https://example.com:443" {
t.Errorf("ManagementURL = %q, want %q", cfg.ManagementURL.String(), "https://example.com:443")
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func TestManagedConfig_KeyConstants(t *testing.T) {
if GetManagedConfigKeyManagementURL() != "managementUrl" {
t.Errorf("unexpected key: %s", GetManagedConfigKeyManagementURL())
}
if GetManagedConfigKeySetupKey() != "setupKey" {
t.Errorf("unexpected key: %s", GetManagedConfigKeySetupKey())
}
if GetManagedConfigKeyAdminURL() != "adminUrl" {
t.Errorf("unexpected key: %s", GetManagedConfigKeyAdminURL())
}
if GetManagedConfigKeyPreSharedKey() != "preSharedKey" {
t.Errorf("unexpected key: %s", GetManagedConfigKeyPreSharedKey())
}
if GetManagedConfigKeyRosenpassEnabled() != "rosenpassEnabled" {
t.Errorf("unexpected key: %s", GetManagedConfigKeyRosenpassEnabled())
}
if GetManagedConfigKeyRosenpassPermissive() != "rosenpassPermissive" {
t.Errorf("unexpected key: %s", GetManagedConfigKeyRosenpassPermissive())
}
if GetManagedConfigKeyDisableAutoConnect() != "disableAutoConnect" {
t.Errorf("unexpected key: %s", GetManagedConfigKeyDisableAutoConnect())
}
}
6 changes: 6 additions & 0 deletions client/ios/NetBirdSDK/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string
return profilemanager.DirectWriteOutConfig(a.cfgPath, a.config)
}

// LoginWithSetupKeySync performs a synchronous setup key login and saves the config.
// This is used by the MDM managed configuration flow where the native app controls threading.
func (a *Auth) LoginWithSetupKeySync(setupKey string, deviceName string) error {
return a.loginWithSetupKeyAndSaveConfig(setupKey, deviceName)
}

// LoginSync performs a synchronous login check without UI interaction
// Used for background VPN connection where user should already be authenticated
func (a *Auth) LoginSync() error {
Expand Down
Loading