[client] Move MDM enforcement logic into a shared Go layer (#7319)

The mobile bridges only carried the policy fetcher, leaving every
enforcement decision to the native apps: the desktop derived its UI
restrictions in the Wails service layer, the daemon kept the conflict
machinery in the server package, and both mobile bridges duplicated the
JSON fetch adapter. Anything the native side had to reimplement was a
place for iOS and Android to drift apart.

Enforcement now lives in client/mdm and is consumed identically by all
three platforms:

- conflicts.go holds the value-aware conflict checks lifted out of the
  daemon, so the same normalization (canonical URLs, PSK sentinel echo)
  applies wherever a config change is validated.
- restrictions.go derives the UI enforcement snapshot from a policy and
  renders it in the JSON shape the desktop frontend already consumes.
  The service-layer types become aliases, keeping one source of truth.
- jsonloader.go replaces the adapter that was copy-pasted into both
  bridges.
- changedetector.go moves change detection off the native side: the
  caller forwards the OS notification and asks whether the managed
  configuration actually changed, instead of diffing dictionaries
  itself.

The mobile bridges gain the enforcement the daemon already had. The
Preferences getters resolve managed keys from the policy, so a naive UI
shows the enforced value; Commit rejects a staged change that diverges
from a managed key; NewAuth resolves the managed management URL before
persisting the config and overlays the policy on it, so a login can no
longer run against a URL the policy forbids. Android's profile
mutations fail closed when disableProfiles is set.

NewAuth takes the fetcher as a required argument rather than keeping a
policy-blind overload: the apps consume this code as a submodule, so a
compile error at the bump is the point. The mobile PSK getter is
replaced by a presence check — the key has no reason to cross the
bridge, and not returning it means the native side needs no redaction
sentinel of its own.
This commit is contained in:
Zoltan Papp
2026-08-26 09:42:13 +02:00
committed by GitHub
parent c41d439185
commit c281b15cfa
20 changed files with 610 additions and 344 deletions

View File

@@ -101,7 +101,8 @@ type Client struct {
// passes this loader to the resolved Config so applyMDMPolicy
// picks up the active overlay. Nil means "MDM enforcement off
// for this Client".
mdmLoader *mdm.Loader
mdmLoader *mdm.Loader
mdmDetector *mdm.ChangeDetector
// Identifies the running profile for the SSO login hint; see profile_state.go.
cfgPath string

View File

@@ -0,0 +1,38 @@
//go:build android
package android
import (
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
)
// SetMDMPolicyFetcher registers the native-provided MDM policy fetcher on
// this Client; passing nil disables MDM enforcement.
func (c *Client) SetMDMPolicyFetcher(p PolicyFetcher) {
c.mdmLoader = loaderFor(p)
c.mdmDetector = mdm.NewChangeDetector(c.mdmLoader)
}
// HasMDMPolicyChanged re-reads the managed configuration and reports whether
// it changed since the last observation; call it from the native OS-change
// notification and restart the engine only on true.
func (c *Client) HasMDMPolicyChanged() bool {
if c.mdmDetector == nil {
return false
}
return c.mdmDetector.Changed()
}
// GetRestrictionsJSON returns the UI enforcement snapshot derived from the
// active MDM policy, in the JSON shape shared with the desktop frontend.
func (c *Client) GetRestrictionsJSON() (string, error) {
return mdm.BuildRestrictions(c.mdmLoader.Load()).JSON()
}
func (c *Client) applyMDMOverlay(cfg *profilemanager.Config) {
if cfg == nil || c.mdmLoader == nil {
return
}
cfg.ApplyMDMPolicy(c.mdmLoader.Load())
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/netbirdio/netbird/client/internal/auth"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
"github.com/netbirdio/netbird/client/system"
)
@@ -45,7 +46,16 @@ type Auth struct {
// an earlier call is orphaned on the server. It also breaks a client that enrols and then runs from
// the persisted config, because the identity it registered is not the one it runs with — the
// management stream rejects it with "no peer auth method provided".
func NewAuth(cfgPath string, mgmURL string) (*Auth, error) {
//
// Auth is constructed under the active MDM policy: a managed management URL
// replaces the caller-supplied one before the config is persisted, and the
// policy is overlaid on the resolved config so the login runs against the
// enforced values. A nil fetcher disables MDM enforcement.
func NewAuth(cfgPath string, mgmURL string, fetcher PolicyFetcher) (*Auth, error) {
policy := loaderFor(fetcher).Load()
if v, ok := policy.GetString(mdm.KeyManagementURL); ok {
mgmURL = v
}
inputCfg := profilemanager.ConfigInput{
ConfigPath: cfgPath,
ManagementURL: mgmURL,
@@ -55,6 +65,7 @@ func NewAuth(cfgPath string, mgmURL string) (*Auth, error) {
if err != nil {
return nil, err
}
cfg.ApplyMDMPolicy(policy)
return &Auth{
ctx: context.Background(),

View File

@@ -16,7 +16,7 @@ import (
func TestNewAuth_ReusesPersistedIdentity(t *testing.T) {
cfgPath := filepath.Join(t.TempDir(), "config.json")
first, err := NewAuth(cfgPath, "https://api.example.com:443")
first, err := NewAuth(cfgPath, "https://api.example.com:443", nil)
if err != nil {
t.Fatalf("first NewAuth: %v", err)
}
@@ -24,7 +24,7 @@ func TestNewAuth_ReusesPersistedIdentity(t *testing.T) {
t.Fatal("first NewAuth produced no private key")
}
second, err := NewAuth(cfgPath, "https://api.example.com:443")
second, err := NewAuth(cfgPath, "https://api.example.com:443", nil)
if err != nil {
t.Fatalf("second NewAuth: %v", err)
}
@@ -38,7 +38,7 @@ func TestNewAuth_ReusesPersistedIdentity(t *testing.T) {
func TestNewAuth_CreatesConfigWhenAbsent(t *testing.T) {
cfgPath := filepath.Join(t.TempDir(), "config.json")
auth, err := NewAuth(cfgPath, "https://api.example.com:443")
auth, err := NewAuth(cfgPath, "https://api.example.com:443", nil)
if err != nil {
t.Fatalf("NewAuth: %v", err)
}

View File

@@ -1,80 +1,19 @@
//go:build android
package android
import (
"encoding/json"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
)
// PolicyFetcher is the mobile-side bridge for the MDM managed-config
// snapshot. The native layer (Kotlin) implements this and registers
// the instance per Client via Client.SetMDMPolicyFetcher. Every
// invocation of fetchJSON must read the current RestrictionsManager
// state and return the result as a JSON-encoded map[string]any string.
//
// JSON is used because gomobile does not support map[string]any
// crossing the JNI boundary — the adapter on the Go side parses the
// string back into the map[string]any expected by mdm.Loader.
//
// Return value contract:
// - "" (empty) : interpreted as "no MDM source / no managed keys"
// - "{}" : managed config explicitly empty
// - "{...}" : JSON object with key/value pairs
// - malformed JSON : logged and treated as empty
// PolicyFetcher is implemented by the native layer to return the current
// managed configuration as a JSON-encoded object string; "" means no MDM
// source is present.
type PolicyFetcher interface {
FetchJSON() string
}
// jsonFetcherAdapter wraps a gomobile-exposed PolicyFetcher into the
// internal mdm.PolicyFetcher interface, taking care of JSON decoding
// on every Fetch.
type jsonFetcherAdapter struct {
inner PolicyFetcher
}
func (a *jsonFetcherAdapter) Fetch() map[string]any {
raw := a.inner.FetchJSON()
if raw == "" {
return nil
}
var out map[string]any
if err := json.Unmarshal([]byte(raw), &out); err != nil {
log.Warnf("MDM mobile fetcher: invalid JSON payload from native: %v", err)
return nil
}
return out
}
// SetMDMPolicyFetcher registers the native-provided MDM policy fetcher
// on this Client. Call once from the gomobile-init code (Kotlin
// Application.onCreate or Service onCreate) before invoking Run /
// RunWithoutLogin. Passing nil disables MDM enforcement on this
// Client.
//
// The fetcher is held as a *mdm.Loader instance on the Client (no
// package-level state) — multiple Clients in the same process get
// independent Loaders, and tests can inject fakes per Client.
func (c *Client) SetMDMPolicyFetcher(p PolicyFetcher) {
func loaderFor(p PolicyFetcher) *mdm.Loader {
if p == nil {
c.mdmLoader = mdm.NewLoader(nil)
return
return mdm.NewJSONLoader(nil)
}
c.mdmLoader = mdm.NewLoader(&jsonFetcherAdapter{inner: p})
}
// applyMDMOverlay applies the Client-held MDM Loader's current policy
// on top of the just-read Config. Called immediately after every
// UpdateOrCreateConfig — profilemanager's apply() initialises the
// policy to empty and leaves overlay responsibility to the lifecycle
// owner. No-op when no fetcher was registered.
func (c *Client) applyMDMOverlay(cfg *profilemanager.Config) {
if cfg == nil || c.mdmLoader == nil {
return
}
cfg.ApplyMDMPolicy(c.mdmLoader.Load())
return mdm.NewJSONLoader(p.FetchJSON)
}

View File

@@ -2,11 +2,13 @@ package android
import (
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
)
// Preferences exports a subset of the internal config for gomobile
type Preferences struct {
configInput profilemanager.ConfigInput
mdmLoader *mdm.Loader
}
// NewPreferences creates a new Preferences instance
@@ -14,11 +16,30 @@ func NewPreferences(configPath string) *Preferences {
ci := profilemanager.ConfigInput{
ConfigPath: configPath,
}
return &Preferences{ci}
return &Preferences{configInput: ci}
}
// SetMDMPolicyFetcher registers the native-provided MDM policy fetcher on
// this Preferences instance; passing nil disables MDM enforcement.
func (p *Preferences) SetMDMPolicyFetcher(f PolicyFetcher) {
p.mdmLoader = loaderFor(f)
}
// GetRestrictionsJSON returns the UI enforcement snapshot derived from the
// active MDM policy, in the JSON shape shared with the desktop frontend.
func (p *Preferences) GetRestrictionsJSON() (string, error) {
return mdm.BuildRestrictions(p.policy()).JSON()
}
func (p *Preferences) policy() *mdm.Policy {
return p.mdmLoader.Load()
}
// GetManagementURL reads URL from config file
func (p *Preferences) GetManagementURL() (string, error) {
if v, ok := p.policy().GetString(mdm.KeyManagementURL); ok {
return mdm.CanonicalURL(v), nil
}
if p.configInput.ManagementURL != "" {
return p.configInput.ManagementURL, nil
}
@@ -53,17 +74,21 @@ func (p *Preferences) SetAdminURL(url string) {
p.configInput.AdminURL = url
}
// GetPreSharedKey reads pre-shared key from config file
func (p *Preferences) GetPreSharedKey() (string, error) {
// HasPreSharedKey reports whether a pre-shared key is staged, persisted, or
// enforced by MDM; the key itself is never handed to the native layer.
func (p *Preferences) HasPreSharedKey() (bool, error) {
if _, ok := p.policy().GetString(mdm.KeyPreSharedKey); ok {
return true, nil
}
if p.configInput.PreSharedKey != nil {
return *p.configInput.PreSharedKey, nil
return *p.configInput.PreSharedKey != "", nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
return false, err
}
return cfg.PreSharedKey, err
return cfg.PreSharedKey != "", nil
}
// SetPreSharedKey stores the given key and waits for commit
@@ -78,6 +103,9 @@ func (p *Preferences) SetRosenpassEnabled(enabled bool) {
// GetRosenpassEnabled reads Rosenpass enabled status from config file
func (p *Preferences) GetRosenpassEnabled() (bool, error) {
if v, ok := p.policy().GetBool(mdm.KeyRosenpassEnabled); ok {
return v, nil
}
if p.configInput.RosenpassEnabled != nil {
return *p.configInput.RosenpassEnabled, nil
}
@@ -96,6 +124,9 @@ func (p *Preferences) SetRosenpassPermissive(permissive bool) {
// GetRosenpassPermissive reads Rosenpass permissive setting from config file
func (p *Preferences) GetRosenpassPermissive() (bool, error) {
if v, ok := p.policy().GetBool(mdm.KeyRosenpassPermissive); ok {
return v, nil
}
if p.configInput.RosenpassPermissive != nil {
return *p.configInput.RosenpassPermissive, nil
}
@@ -109,6 +140,9 @@ func (p *Preferences) GetRosenpassPermissive() (bool, error) {
// GetDisableClientRoutes reads disable client routes setting from config file
func (p *Preferences) GetDisableClientRoutes() (bool, error) {
if v, ok := p.policy().GetBool(mdm.KeyDisableClientRoutes); ok {
return v, nil
}
if p.configInput.DisableClientRoutes != nil {
return *p.configInput.DisableClientRoutes, nil
}
@@ -127,6 +161,9 @@ func (p *Preferences) SetDisableClientRoutes(disable bool) {
// GetDisableServerRoutes reads disable server routes setting from config file
func (p *Preferences) GetDisableServerRoutes() (bool, error) {
if v, ok := p.policy().GetBool(mdm.KeyDisableServerRoutes); ok {
return v, nil
}
if p.configInput.DisableServerRoutes != nil {
return *p.configInput.DisableServerRoutes, nil
}
@@ -181,6 +218,9 @@ func (p *Preferences) SetDisableFirewall(disable bool) {
// GetServerSSHAllowed reads server SSH allowed setting from config file
func (p *Preferences) GetServerSSHAllowed() (bool, error) {
if v, ok := p.policy().GetBool(mdm.KeyAllowServerSSH); ok {
return v, nil
}
if p.configInput.ServerSSHAllowed != nil {
return *p.configInput.ServerSSHAllowed, nil
}
@@ -291,6 +331,9 @@ func (p *Preferences) SetEnableSSHRemotePortForwarding(enabled bool) {
// GetBlockInbound reads block inbound setting from config file
func (p *Preferences) GetBlockInbound() (bool, error) {
if v, ok := p.policy().GetBool(mdm.KeyBlockInbound); ok {
return v, nil
}
if p.configInput.BlockInbound != nil {
return *p.configInput.BlockInbound, nil
}
@@ -327,6 +370,9 @@ func (p *Preferences) SetDisableIPv6(disable bool) {
// Commit writes out the changes to the config file
func (p *Preferences) Commit() error {
if err := profilemanager.CheckMDMConflicts(p.configInput, p.policy()); err != nil {
return err
}
_, err := profilemanager.UpdateOrCreateConfig(p.configInput)
return err
}

View File

@@ -28,14 +28,13 @@ func TestPreferences_DefaultValues(t *testing.T) {
t.Errorf("invalid default management url: %s", defaultVar)
}
var preSharedKey string
preSharedKey, err = p.GetPreSharedKey()
hasPSK, err := p.HasPreSharedKey()
if err != nil {
t.Fatalf("failed to read default preshared key: %s", err)
t.Fatalf("failed to read default preshared key presence: %s", err)
}
if preSharedKey != "" {
t.Errorf("invalid preshared key: %s", preSharedKey)
if hasPSK {
t.Errorf("unexpected preshared key presence on fresh config")
}
}
@@ -65,13 +64,13 @@ func TestPreferences_ReadUncommitedValues(t *testing.T) {
}
p.SetPreSharedKey(exampleString)
resp, err = p.GetPreSharedKey()
hasPSK, err := p.HasPreSharedKey()
if err != nil {
t.Fatalf("failed to read preshared key: %s", err)
t.Fatalf("failed to read preshared key presence: %s", err)
}
if resp != exampleString {
t.Errorf("unexpected preshared key: %s", resp)
if !hasPSK {
t.Errorf("expected preshared key presence after staging one")
}
}
@@ -109,12 +108,12 @@ func TestPreferences_Commit(t *testing.T) {
t.Errorf("unexpected management url: %s", resp)
}
resp, err = p.GetPreSharedKey()
hasPSK, err := p.HasPreSharedKey()
if err != nil {
t.Fatalf("failed to read preshared key: %s", err)
t.Fatalf("failed to read preshared key presence: %s", err)
}
if resp != examplePresharedKey {
t.Errorf("unexpected preshared key: %s", resp)
if !hasPSK {
t.Errorf("expected preshared key presence after commit")
}
}

View File

@@ -3,6 +3,7 @@
package android
import (
"errors"
"fmt"
"os"
"path/filepath"
@@ -10,6 +11,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
)
const (
@@ -17,6 +19,9 @@ const (
androidUsername = "android"
)
// ErrProfilesDisabled marks a profile mutation rejected by MDM policy.
var ErrProfilesDisabled = errors.New("profile management is disabled by MDM policy")
// Profile represents a profile for gomobile
type Profile struct {
ID string
@@ -64,6 +69,7 @@ func (p *ProfileArray) Get(i int) *Profile {
type ProfileManager struct {
configDir string
serviceMgr *profilemanager.ServiceManager
mdmLoader *mdm.Loader
}
// NewProfileManager creates a new profile manager for Android
@@ -144,6 +150,9 @@ func (pm *ProfileManager) profileEmail(id string) string {
// SwitchProfile switches to a different profile
func (pm *ProfileManager) SwitchProfile(id string) error {
if err := pm.checkProfilesAllowed(); err != nil {
return err
}
// Use ServiceManager to stay consistent with ListProfiles
// ServiceManager uses active_profile.json
err := pm.serviceMgr.SetActiveProfileState(&profilemanager.ActiveProfileState{
@@ -160,6 +169,9 @@ func (pm *ProfileManager) SwitchProfile(id string) error {
// AddProfile creates a new profile
func (pm *ProfileManager) AddProfile(profileName string) error {
if err := pm.checkProfilesAllowed(); err != nil {
return err
}
// Use ServiceManager (creates profile in profiles/ directory)
profile, err := pm.serviceMgr.AddProfile(profileName, androidUsername)
if err != nil {
@@ -213,6 +225,9 @@ func (pm *ProfileManager) LogoutProfile(id string) error {
// is rewritten. This works for the default profile too, whose config lives in
// netbird.cfg rather than under profiles/.
func (pm *ProfileManager) RenameProfile(id string, newName string) error {
if err := pm.checkProfilesAllowed(); err != nil {
return err
}
if err := pm.serviceMgr.RenameProfile(profilemanager.ID(id), androidUsername, newName); err != nil {
return fmt.Errorf("failed to rename profile: %w", err)
}
@@ -223,6 +238,9 @@ func (pm *ProfileManager) RenameProfile(id string, newName string) error {
// RemoveProfile deletes a profile
func (pm *ProfileManager) RemoveProfile(id string) error {
if err := pm.checkProfilesAllowed(); err != nil {
return err
}
configPath, err := pm.getProfileConfigPath(id)
if err != nil {
return err
@@ -245,6 +263,19 @@ func (pm *ProfileManager) RemoveProfile(id string) error {
return nil
}
// SetMDMPolicyFetcher registers the native-provided MDM policy fetcher on
// this ProfileManager; passing nil disables MDM enforcement.
func (pm *ProfileManager) SetMDMPolicyFetcher(f PolicyFetcher) {
pm.mdmLoader = loaderFor(f)
}
func (pm *ProfileManager) checkProfilesAllowed() error {
if v, ok := pm.mdmLoader.Load().GetBool(mdm.KeyDisableProfiles); ok && v {
return ErrProfilesDisabled
}
return nil
}
// getProfileConfigPath returns the config file path for a profile
// This is needed for Android-specific path handling (netbird.cfg for default profile)
func (pm *ProfileManager) getProfileConfigPath(id string) (string, error) {

View File

@@ -0,0 +1,49 @@
package profilemanager
import (
"errors"
"fmt"
"github.com/netbirdio/netbird/client/mdm"
)
// ErrMDMManagedFields marks a config change rejected because it diverges from
// MDM-enforced values.
var ErrMDMManagedFields = errors.New("fields managed by MDM cannot be modified")
// MDMConflicts returns the names of MDM-managed keys whose requested value in
// the ConfigInput differs from the policy-enforced value; a field set to the
// enforced value is a no-op echo, not a conflict.
func MDMConflicts(input ConfigInput, policy *mdm.Policy) []string {
pskGot := ""
if input.PreSharedKey != nil && !isPreSharedKeyHidden(input.PreSharedKey) {
pskGot = *input.PreSharedKey
}
var port *int64
if input.WireguardPort != nil {
v := int64(*input.WireguardPort)
port = &v
}
return mdm.ResolveConflicts(policy, []mdm.ConflictCheck{
mdm.ConflictURL(mdm.KeyManagementURL, input.ManagementURL),
mdm.ConflictString(mdm.KeyPreSharedKey, pskGot),
mdm.ConflictBool(mdm.KeyRosenpassEnabled, input.RosenpassEnabled),
mdm.ConflictBool(mdm.KeyRosenpassPermissive, input.RosenpassPermissive),
mdm.ConflictBool(mdm.KeyDisableAutoConnect, input.DisableAutoConnect),
mdm.ConflictBool(mdm.KeyAllowServerSSH, input.ServerSSHAllowed),
mdm.ConflictBool(mdm.KeyDisableClientRoutes, input.DisableClientRoutes),
mdm.ConflictBool(mdm.KeyDisableServerRoutes, input.DisableServerRoutes),
mdm.ConflictBool(mdm.KeyBlockInbound, input.BlockInbound),
mdm.ConflictInt64(mdm.KeyWireguardPort, port),
})
}
// CheckMDMConflicts returns an ErrMDMManagedFields-wrapped error naming the
// conflicting keys, or nil when the input does not fight the policy.
func CheckMDMConflicts(input ConfigInput, policy *mdm.Policy) error {
conflicts := MDMConflicts(input, policy)
if len(conflicts) == 0 {
return nil
}
return fmt.Errorf("%w: %v", ErrMDMManagedFields, conflicts)
}

View File

@@ -99,7 +99,8 @@ type Client struct {
// init). Each Run passes this loader to the resolved Config so
// applyMDMPolicy picks up the active overlay. Nil means "MDM
// enforcement off for this Client".
mdmLoader *mdm.Loader
mdmLoader *mdm.Loader
mdmDetector *mdm.ChangeDetector
stateMu sync.RWMutex
connectClient *internal.ConnectClient

View File

@@ -11,6 +11,7 @@ import (
"github.com/netbirdio/netbird/client/internal/auth"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
"github.com/netbirdio/netbird/client/system"
)
@@ -41,8 +42,16 @@ type Auth struct {
cfgPath string
}
// NewAuth instantiate Auth struct and validate the management URL
func NewAuth(cfgPath string, mgmURL string) (*Auth, error) {
// NewAuth instantiate Auth struct and validate the management URL.
// Auth is constructed under the active MDM policy: a managed management URL
// replaces the caller-supplied one before the config is persisted, and the
// policy is overlaid on the resolved config so the login runs against the
// enforced values. A nil fetcher disables MDM enforcement.
func NewAuth(cfgPath string, mgmURL string, fetcher PolicyFetcher) (*Auth, error) {
policy := loaderFor(fetcher).Load()
if v, ok := policy.GetString(mdm.KeyManagementURL); ok {
mgmURL = v
}
inputCfg := profilemanager.ConfigInput{
ConfigPath: cfgPath,
ManagementURL: mgmURL,
@@ -66,6 +75,7 @@ func NewAuth(cfgPath string, mgmURL string) (*Auth, error) {
if err != nil {
return nil, err
}
cfg.ApplyMDMPolicy(policy)
// Use a cancellable context so Stop() can abort an in-progress interactive
// login. The PKCE flow's WaitToken blocks (and keeps its loopback HTTP server

View File

@@ -3,80 +3,50 @@
package NetBirdSDK
import (
"encoding/json"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
)
// PolicyFetcher is the mobile-side bridge for the MDM managed-config
// snapshot. The native layer (Swift) implements this and registers
// the instance per Client via Client.SetMDMPolicyFetcher. Every
// invocation of fetchJSON must read the current
// UserDefaults.standard.dictionary(forKey: "com.apple.configuration.managed")
// and return the result as a JSON-encoded map[string]any string.
//
// JSON is used because gomobile does not support map[string]any
// crossing the Objective-C boundary — the adapter on the Go side
// parses the string back into the map[string]any expected by
// mdm.Loader.
//
// Return value contract:
// - "" (empty) : interpreted as "no MDM source / no managed keys"
// - "{}" : managed config explicitly empty
// - "{...}" : JSON object with key/value pairs
// - malformed JSON : logged and treated as empty
// PolicyFetcher is implemented by the native layer to return the current
// managed configuration as a JSON-encoded object string; "" means no MDM
// source is present.
type PolicyFetcher interface {
FetchJSON() string
}
// jsonFetcherAdapter wraps a gomobile-exposed PolicyFetcher into the
// internal mdm.PolicyFetcher interface, taking care of JSON decoding
// on every Fetch.
type jsonFetcherAdapter struct {
inner PolicyFetcher
}
func (a *jsonFetcherAdapter) Fetch() map[string]any {
raw := a.inner.FetchJSON()
if raw == "" {
return nil
}
var out map[string]any
if err := json.Unmarshal([]byte(raw), &out); err != nil {
log.Warnf("MDM mobile fetcher: invalid JSON payload from native: %v", err)
return nil
}
return out
}
// SetMDMPolicyFetcher registers the native-provided MDM policy fetcher
// on this Client. Call once from the gomobile-init code (Swift
// AppDelegate / PacketTunnelProvider.startTunnel) before invoking Run.
// Passing nil disables MDM enforcement on this Client.
//
// The fetcher is held as a *mdm.Loader instance on the Client (no
// package-level state) — multiple Clients in the same process get
// independent Loaders, and tests can inject fakes per Client.
// SetMDMPolicyFetcher registers the native-provided MDM policy fetcher on
// this Client; passing nil disables MDM enforcement.
func (c *Client) SetMDMPolicyFetcher(p PolicyFetcher) {
if p == nil {
c.mdmLoader = mdm.NewLoader(nil)
return
}
c.mdmLoader = mdm.NewLoader(&jsonFetcherAdapter{inner: p})
c.mdmLoader = loaderFor(p)
c.mdmDetector = mdm.NewChangeDetector(c.mdmLoader)
}
// HasMDMPolicyChanged re-reads the managed configuration and reports whether
// it changed since the last observation; call it from the native OS-change
// notification and restart the engine only on true.
func (c *Client) HasMDMPolicyChanged() bool {
if c.mdmDetector == nil {
return false
}
return c.mdmDetector.Changed()
}
// GetRestrictionsJSON returns the UI enforcement snapshot derived from the
// active MDM policy, in the JSON shape shared with the desktop frontend.
func (c *Client) GetRestrictionsJSON() (string, error) {
return mdm.BuildRestrictions(c.mdmLoader.Load()).JSON()
}
// applyMDMOverlay applies the Client-held MDM Loader's current policy
// on top of the just-read Config. Called immediately after every
// UpdateOrCreateConfig / DirectUpdateOrCreateConfig — profilemanager's
// apply() initialises the policy to empty and leaves overlay
// responsibility to the lifecycle owner. No-op when no fetcher was
// registered.
func (c *Client) applyMDMOverlay(cfg *profilemanager.Config) {
if cfg == nil || c.mdmLoader == nil {
return
}
cfg.ApplyMDMPolicy(c.mdmLoader.Load())
}
func loaderFor(p PolicyFetcher) *mdm.Loader {
if p == nil {
return mdm.NewJSONLoader(nil)
}
return mdm.NewJSONLoader(p.FetchJSON)
}

View File

@@ -4,11 +4,13 @@ package NetBirdSDK
import (
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
)
// Preferences export a subset of the internal config for gomobile
type Preferences struct {
configInput profilemanager.ConfigInput
mdmLoader *mdm.Loader
}
// NewPreferences create new Preferences instance
@@ -17,11 +19,30 @@ func NewPreferences(configPath string, stateFilePath string) *Preferences {
ConfigPath: configPath,
StateFilePath: stateFilePath,
}
return &Preferences{ci}
return &Preferences{configInput: ci}
}
// SetMDMPolicyFetcher registers the native-provided MDM policy fetcher on
// this Preferences instance; passing nil disables MDM enforcement.
func (p *Preferences) SetMDMPolicyFetcher(f PolicyFetcher) {
p.mdmLoader = loaderFor(f)
}
// GetRestrictionsJSON returns the UI enforcement snapshot derived from the
// active MDM policy, in the JSON shape shared with the desktop frontend.
func (p *Preferences) GetRestrictionsJSON() (string, error) {
return mdm.BuildRestrictions(p.policy()).JSON()
}
func (p *Preferences) policy() *mdm.Policy {
return p.mdmLoader.Load()
}
// GetManagementURL read url from config file
func (p *Preferences) GetManagementURL() (string, error) {
if v, ok := p.policy().GetString(mdm.KeyManagementURL); ok {
return mdm.CanonicalURL(v), nil
}
if p.configInput.ManagementURL != "" {
return p.configInput.ManagementURL, nil
}
@@ -56,17 +77,21 @@ func (p *Preferences) SetAdminURL(url string) {
p.configInput.AdminURL = url
}
// GetPreSharedKey read preshared key from config file
func (p *Preferences) GetPreSharedKey() (string, error) {
// HasPreSharedKey reports whether a pre-shared key is staged, persisted, or
// enforced by MDM; the key itself is never handed to the native layer.
func (p *Preferences) HasPreSharedKey() (bool, error) {
if _, ok := p.policy().GetString(mdm.KeyPreSharedKey); ok {
return true, nil
}
if p.configInput.PreSharedKey != nil {
return *p.configInput.PreSharedKey, nil
return *p.configInput.PreSharedKey != "", nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
return false, err
}
return cfg.PreSharedKey, err
return cfg.PreSharedKey != "", nil
}
// SetPreSharedKey store the given key and wait for commit
@@ -81,6 +106,9 @@ func (p *Preferences) SetRosenpassEnabled(enabled bool) {
// GetRosenpassEnabled read rosenpass enabled from config file
func (p *Preferences) GetRosenpassEnabled() (bool, error) {
if v, ok := p.policy().GetBool(mdm.KeyRosenpassEnabled); ok {
return v, nil
}
if p.configInput.RosenpassEnabled != nil {
return *p.configInput.RosenpassEnabled, nil
}
@@ -99,6 +127,9 @@ func (p *Preferences) SetRosenpassPermissive(permissive bool) {
// GetRosenpassPermissive read rosenpass permissive from config file
func (p *Preferences) GetRosenpassPermissive() (bool, error) {
if v, ok := p.policy().GetBool(mdm.KeyRosenpassPermissive); ok {
return v, nil
}
if p.configInput.RosenpassPermissive != nil {
return *p.configInput.RosenpassPermissive, nil
}
@@ -130,6 +161,9 @@ func (p *Preferences) SetDisableIPv6(disable bool) {
// Commit write out the changes into config file
func (p *Preferences) Commit() error {
if err := profilemanager.CheckMDMConflicts(p.configInput, p.policy()); err != nil {
return err
}
// Use DirectUpdateOrCreateConfig to avoid atomic file operations (temp file + rename)
// which are blocked by the tvOS sandbox in App Group containers
_, err := profilemanager.DirectUpdateOrCreateConfig(p.configInput)

View File

@@ -31,14 +31,13 @@ func TestPreferences_DefaultValues(t *testing.T) {
t.Errorf("invalid default management url: %s", defaultVar)
}
var preSharedKey string
preSharedKey, err = p.GetPreSharedKey()
hasPSK, err := p.HasPreSharedKey()
if err != nil {
t.Fatalf("failed to read default preshared key: %s", err)
t.Fatalf("failed to read default preshared key presence: %s", err)
}
if preSharedKey != "" {
t.Errorf("invalid preshared key: %s", preSharedKey)
if hasPSK {
t.Errorf("unexpected preshared key presence on fresh config")
}
}
@@ -69,13 +68,13 @@ func TestPreferences_ReadUncommitedValues(t *testing.T) {
}
p.SetPreSharedKey(exampleString)
resp, err = p.GetPreSharedKey()
hasPSK, err := p.HasPreSharedKey()
if err != nil {
t.Fatalf("failed to read preshared key: %s", err)
t.Fatalf("failed to read preshared key presence: %s", err)
}
if resp != exampleString {
t.Errorf("unexpected preshared key: %s", resp)
if !hasPSK {
t.Errorf("expected preshared key presence after staging one")
}
}
@@ -114,12 +113,12 @@ func TestPreferences_Commit(t *testing.T) {
t.Errorf("unexpected management url: %s", resp)
}
resp, err = p.GetPreSharedKey()
hasPSK, err := p.HasPreSharedKey()
if err != nil {
t.Fatalf("failed to read preshared key: %s", err)
t.Fatalf("failed to read preshared key presence: %s", err)
}
if resp != examplePresharedKey {
t.Errorf("unexpected preshared key: %s", resp)
if !hasPSK {
t.Errorf("expected preshared key presence after commit")
}
}

View File

@@ -0,0 +1,47 @@
package mdm
import (
"sync"
log "github.com/sirupsen/logrus"
)
// ChangeDetector tracks the last observed policy of a Loader so an
// OS-notification-driven caller can ask whether the managed configuration
// actually changed before restarting anything.
type ChangeDetector struct {
mu sync.Mutex
loader *Loader
prev *Policy
}
// NewChangeDetector constructs a ChangeDetector seeded with the loader's
// current policy, so only a later change reports as changed.
func NewChangeDetector(loader *Loader) *ChangeDetector {
return &ChangeDetector{
loader: loader,
prev: loader.Load(),
}
}
// Changed re-reads the policy, logs the per-key diff, and reports whether it
// diverged from the last observation; the new snapshot becomes the baseline.
func (d *ChangeDetector) Changed() bool {
d.mu.Lock()
defer d.mu.Unlock()
curr := d.loader.Load()
if policiesEqual(d.prev, curr) {
return false
}
added, removed, changed := diffPolicies(d.prev, curr)
log.Infof("MDM policy changed: added=%v removed=%v changed=%v", added, removed, changed)
d.prev = curr
return true
}
// Current returns the last observed policy snapshot.
func (d *ChangeDetector) Current() *Policy {
d.mu.Lock()
defer d.mu.Unlock()
return d.prev
}

108
client/mdm/conflicts.go Normal file
View File

@@ -0,0 +1,108 @@
package mdm
import "net/url"
// PreSharedKeyRedactedSentinel is the redaction mask returned in place of a
// real pre-shared key; an incoming value equal to it is a round-trip echo,
// never an override.
const PreSharedKeyRedactedSentinel = "**********"
// ConflictCheck is a value-aware comparison between a single requested field
// and the corresponding MDM-enforced value.
type ConflictCheck struct {
Key string
Check func(*Policy) bool
}
// ConflictBool builds a ConflictCheck for a boolean MDM key.
func ConflictBool(key string, p *bool) ConflictCheck {
return ConflictCheck{
Key: key,
Check: func(pol *Policy) bool {
if p == nil {
return true
}
want, ok := pol.GetBool(key)
return ok && want == *p
},
}
}
// ConflictString builds a ConflictCheck for a string MDM key.
func ConflictString(key, got string) ConflictCheck {
return ConflictCheck{
Key: key,
Check: func(pol *Policy) bool {
if got == "" {
return true
}
want, ok := pol.GetString(key)
return ok && want == got
},
}
}
// ConflictURL builds a ConflictCheck for a URL-typed MDM key; both sides are
// normalized via CanonicalURL before comparison.
func ConflictURL(key, got string) ConflictCheck {
return ConflictCheck{
Key: key,
Check: func(pol *Policy) bool {
if got == "" {
return true
}
want, ok := pol.GetString(key)
return ok && CanonicalURL(want) == CanonicalURL(got)
},
}
}
// ConflictInt64 builds a ConflictCheck for an integer MDM key.
func ConflictInt64(key string, p *int64) ConflictCheck {
return ConflictCheck{
Key: key,
Check: func(pol *Policy) bool {
if p == nil {
return true
}
want, ok := pol.GetInt(key)
return ok && want == *p
},
}
}
// ResolveConflicts returns the names of keys whose requested value diverges
// from the policy-enforced value; keys the policy does not manage are skipped.
func ResolveConflicts(policy *Policy, checks []ConflictCheck) []string {
if policy.IsEmpty() {
return nil
}
var conflicts []string
for _, c := range checks {
if !policy.HasKey(c.Key) {
continue
}
if !c.Check(policy) {
conflicts = append(conflicts, c.Key)
}
}
return conflicts
}
// CanonicalURL normalizes a service URL by appending the scheme default port
// when none is present; unparseable input is returned unchanged.
func CanonicalURL(s string) string {
u, err := url.ParseRequestURI(s)
if err != nil {
return s
}
if u.Port() == "" {
switch u.Scheme {
case "https":
u.Host += ":443"
case "http":
u.Host += ":80"
}
}
return u.String()
}

34
client/mdm/jsonloader.go Normal file
View File

@@ -0,0 +1,34 @@
package mdm
import (
"encoding/json"
log "github.com/sirupsen/logrus"
)
type jsonPolicyFetcher struct {
fetch func() string
}
// NewJSONLoader constructs a Loader whose policy source is a JSON-encoded
// object string, as produced by the mobile native layers; a nil fetch
// disables MDM enforcement.
func NewJSONLoader(fetch func() string) *Loader {
if fetch == nil {
return NewLoader(nil)
}
return NewLoader(&jsonPolicyFetcher{fetch: fetch})
}
func (f *jsonPolicyFetcher) Fetch() map[string]any {
raw := f.fetch()
if raw == "" {
return nil
}
var out map[string]any
if err := json.Unmarshal([]byte(raw), &out); err != nil {
log.Warnf("MDM mobile fetcher: invalid JSON payload from native: %v", err)
return nil
}
return out
}

View File

@@ -0,0 +1,89 @@
package mdm
import "encoding/json"
// Fields carries the per-key MDM enforcement state for a UI: value-typed
// fields hold the enforced value, boolean fields report that the key is
// managed.
type Fields struct {
ManagementURL string `json:"managementURL"`
PreSharedKey bool `json:"preSharedKey"`
WireguardPort bool `json:"wireguardPort"`
RosenpassEnabled bool `json:"rosenpassEnabled"`
RosenpassPermissive bool `json:"rosenpassPermissive"`
DisableClientRoutes bool `json:"disableClientRoutes"`
DisableServerRoutes bool `json:"disableServerRoutes"`
AllowServerSSH *bool `json:"allowServerSSH"`
DisableAutoConnect bool `json:"disableAutoConnect"`
DisableAutostart bool `json:"disableAutostart"`
BlockInbound bool `json:"blockInbound"`
DisableMetricsCollection bool `json:"disableMetricsCollection"`
SplitTunnelMode bool `json:"splitTunnelMode"`
SplitTunnelApps bool `json:"splitTunnelApps"`
DisableAdvancedView bool `json:"disableAdvancedView"`
}
// Features carries the feature gates a UI must honor.
type Features struct {
DisableProfiles bool `json:"disableProfiles"`
DisableNetworks bool `json:"disableNetworks"`
DisableUpdateSettings bool `json:"disableUpdateSettings"`
}
// Restrictions is the UI-facing enforcement snapshot; the JSON shape is
// shared by the desktop frontend and the mobile bridges.
type Restrictions struct {
MDM Fields `json:"mdm"`
Features Features `json:"features"`
}
// BuildRestrictions derives the UI enforcement snapshot from the active
// policy.
func BuildRestrictions(policy *Policy) Restrictions {
var r Restrictions
if policy.IsEmpty() {
return r
}
if v, ok := policy.GetString(KeyManagementURL); ok {
r.MDM.ManagementURL = CanonicalURL(v)
}
r.MDM.PreSharedKey = policy.HasKey(KeyPreSharedKey)
r.MDM.WireguardPort = policy.HasKey(KeyWireguardPort)
r.MDM.RosenpassEnabled = policy.HasKey(KeyRosenpassEnabled)
r.MDM.RosenpassPermissive = policy.HasKey(KeyRosenpassPermissive)
r.MDM.DisableClientRoutes = policy.HasKey(KeyDisableClientRoutes)
r.MDM.DisableServerRoutes = policy.HasKey(KeyDisableServerRoutes)
r.MDM.DisableAutoConnect = policy.HasKey(KeyDisableAutoConnect)
r.MDM.DisableAutostart = policy.HasKey(KeyDisableAutostart)
r.MDM.BlockInbound = policy.HasKey(KeyBlockInbound)
r.MDM.DisableMetricsCollection = policy.HasKey(KeyDisableMetricsCollection)
r.MDM.SplitTunnelMode = policy.HasKey(KeySplitTunnelMode)
r.MDM.SplitTunnelApps = policy.HasKey(KeySplitTunnelApps)
if v, ok := policy.GetBool(KeyAllowServerSSH); ok {
r.MDM.AllowServerSSH = &v
}
if v, ok := policy.GetBool(KeyDisableAdvancedView); ok {
r.MDM.DisableAdvancedView = v
}
if v, ok := policy.GetBool(KeyDisableProfiles); ok {
r.Features.DisableProfiles = v
}
if v, ok := policy.GetBool(KeyDisableNetworks); ok {
r.Features.DisableNetworks = v
}
if v, ok := policy.GetBool(KeyDisableUpdateSettings); ok {
r.Features.DisableUpdateSettings = v
}
return r
}
// JSON renders the snapshot in the shared UI JSON shape.
func (r Restrictions) JSON() (string, error) {
b, err := json.Marshal(r)
if err != nil {
return "", err
}
return string(b), nil
}

View File

@@ -3,7 +3,6 @@ package server
import (
"context"
"fmt"
"net/url"
"time"
log "github.com/sirupsen/logrus"
@@ -14,24 +13,6 @@ import (
"github.com/netbirdio/netbird/client/proto"
)
// preSharedKeyRedactedSentinel is the value GetConfig returns in place
// of an actual PSK, so a UI that round-trips the field back to the
// daemon (via SetConfig / Login) can be distinguished from a deliberate
// override. Any incoming PSK that equals this sentinel is treated as
// a no-op echo, never as a conflict with the policy.
const preSharedKeyRedactedSentinel = "**********"
// conflictCheck is a value-aware comparison between a single field in
// the incoming request and the corresponding MDM-enforced value. It
// runs only when the field was actually set in the request (presence
// already filtered upstream); ok=true reports the policy value, ok=false
// means the policy is silent on the key — both are treated as conflicts
// to be safe (an MDM key declared as managed must hold a value).
type conflictCheck struct {
key string
check func(*mdm.Policy) (match bool)
}
// onMDMPolicyChange is invoked by the MDM reload ticker every time the
// OS-native managed-config store reports a diff vs the last observation.
//
@@ -164,108 +145,6 @@ func (s *Server) restartEngineForMDMLocked() error {
return nil
}
// conflictBool builds a conflictCheck for a boolean MDM key. If p is nil
// the field is treated as matching (no override requested); otherwise the
// check returns true only when the policy contains the key and its
// boolean value equals *p.
func conflictBool(key string, p *bool) conflictCheck {
return conflictCheck{
key: key,
check: func(pol *mdm.Policy) bool {
if p == nil {
return true // absent → match by definition
}
want, ok := pol.GetBool(key)
return ok && want == *p
},
}
}
func canonicalURL(s string) string {
u, err := url.ParseRequestURI(s)
if err != nil {
return s
}
if u.Port() == "" {
switch u.Scheme {
case "https":
u.Host += ":443"
case "http":
u.Host += ":80"
}
}
return u.String()
}
// conflictURL is conflictString for URL-typed keys: both sides are
// normalized via canonicalURL before comparison.
func conflictURL(key, got string) conflictCheck {
return conflictCheck{
key: key,
check: func(pol *mdm.Policy) bool {
if got == "" {
return true
}
want, ok := pol.GetString(key)
return ok && canonicalURL(want) == canonicalURL(got)
},
}
}
// conflictString builds a conflictCheck for a string MDM key. An empty
// `got` is treated as "field not set" (no override requested); otherwise
// the check returns true only when the policy contains the key and its
// value equals got.
func conflictString(key, got string) conflictCheck {
return conflictCheck{
key: key,
check: func(pol *mdm.Policy) bool {
if got == "" {
return true
}
want, ok := pol.GetString(key)
return ok && want == got
},
}
}
// conflictInt64 builds a conflictCheck for an integer MDM key. If p is
// nil the field is treated as matching; otherwise the check returns
// true only when the policy contains the key and its int value equals *p.
func conflictInt64(key string, p *int64) conflictCheck {
return conflictCheck{
key: key,
check: func(pol *mdm.Policy) bool {
if p == nil {
return true
}
want, ok := pol.GetInt(key)
return ok && want == *p
},
}
}
// resolveConflicts walks the per-field checks against the active MDM
// policy and returns the names of keys whose requested value diverges
// from the policy-enforced value. Keys not present in the policy are
// skipped silently (the gate fires only for keys the admin has
// actually pushed). Returns nil for an empty policy.
func resolveConflicts(policy *mdm.Policy, checks []conflictCheck) []string {
if policy.IsEmpty() {
return nil
}
var conflicts []string
for _, c := range checks {
if !policy.HasKey(c.key) {
continue
}
if !c.check(policy) {
conflicts = append(conflicts, c.key)
}
}
return conflicts
}
// mdmManagedFieldConflicts returns the names of MDM-managed keys whose
// requested value in the SetConfigRequest differs from the MDM-enforced
// value. A field set to the same value the policy already enforces is
@@ -282,21 +161,21 @@ func mdmManagedFieldConflicts(msg *proto.SetConfigRequest, policy *mdm.Policy) [
// PSK round-trip echo: collapse the sentinel to empty so the
// shared check treats it as "field not set".
pskGot := ""
if msg.OptionalPreSharedKey != nil && *msg.OptionalPreSharedKey != preSharedKeyRedactedSentinel {
if msg.OptionalPreSharedKey != nil && *msg.OptionalPreSharedKey != mdm.PreSharedKeyRedactedSentinel {
pskGot = *msg.OptionalPreSharedKey
}
return resolveConflicts(policy, []conflictCheck{
conflictURL(mdm.KeyManagementURL, msg.ManagementUrl),
conflictString(mdm.KeyPreSharedKey, pskGot),
conflictBool(mdm.KeyRosenpassEnabled, msg.RosenpassEnabled),
conflictBool(mdm.KeyRosenpassPermissive, msg.RosenpassPermissive),
conflictBool(mdm.KeyDisableAutoConnect, msg.DisableAutoConnect),
conflictBool(mdm.KeyAllowServerSSH, msg.ServerSSHAllowed),
conflictBool(mdm.KeyDisableClientRoutes, msg.DisableClientRoutes),
conflictBool(mdm.KeyDisableServerRoutes, msg.DisableServerRoutes),
conflictBool(mdm.KeyBlockInbound, msg.BlockInbound),
conflictInt64(mdm.KeyWireguardPort, msg.WireguardPort),
return mdm.ResolveConflicts(policy, []mdm.ConflictCheck{
mdm.ConflictURL(mdm.KeyManagementURL, msg.ManagementUrl),
mdm.ConflictString(mdm.KeyPreSharedKey, pskGot),
mdm.ConflictBool(mdm.KeyRosenpassEnabled, msg.RosenpassEnabled),
mdm.ConflictBool(mdm.KeyRosenpassPermissive, msg.RosenpassPermissive),
mdm.ConflictBool(mdm.KeyDisableAutoConnect, msg.DisableAutoConnect),
mdm.ConflictBool(mdm.KeyAllowServerSSH, msg.ServerSSHAllowed),
mdm.ConflictBool(mdm.KeyDisableClientRoutes, msg.DisableClientRoutes),
mdm.ConflictBool(mdm.KeyDisableServerRoutes, msg.DisableServerRoutes),
mdm.ConflictBool(mdm.KeyBlockInbound, msg.BlockInbound),
mdm.ConflictInt64(mdm.KeyWireguardPort, msg.WireguardPort),
})
}
@@ -403,21 +282,21 @@ func loginRequestMDMConflicts(msg *proto.LoginRequest, policy *mdm.Policy) []str
} else if msg.PreSharedKey != "" { //nolint:staticcheck // SA1019: legacy proto field still accepted by Login
pskGot = msg.PreSharedKey //nolint:staticcheck // SA1019
}
if pskGot == preSharedKeyRedactedSentinel {
if pskGot == mdm.PreSharedKeyRedactedSentinel {
pskGot = ""
}
return resolveConflicts(policy, []conflictCheck{
conflictURL(mdm.KeyManagementURL, msg.ManagementUrl),
conflictString(mdm.KeyPreSharedKey, pskGot),
conflictBool(mdm.KeyRosenpassEnabled, msg.RosenpassEnabled),
conflictBool(mdm.KeyRosenpassPermissive, msg.RosenpassPermissive),
conflictBool(mdm.KeyDisableAutoConnect, msg.DisableAutoConnect),
conflictBool(mdm.KeyAllowServerSSH, msg.ServerSSHAllowed),
conflictBool(mdm.KeyDisableClientRoutes, msg.DisableClientRoutes),
conflictBool(mdm.KeyDisableServerRoutes, msg.DisableServerRoutes),
conflictBool(mdm.KeyBlockInbound, msg.BlockInbound),
conflictInt64(mdm.KeyWireguardPort, msg.WireguardPort),
return mdm.ResolveConflicts(policy, []mdm.ConflictCheck{
mdm.ConflictURL(mdm.KeyManagementURL, msg.ManagementUrl),
mdm.ConflictString(mdm.KeyPreSharedKey, pskGot),
mdm.ConflictBool(mdm.KeyRosenpassEnabled, msg.RosenpassEnabled),
mdm.ConflictBool(mdm.KeyRosenpassPermissive, msg.RosenpassPermissive),
mdm.ConflictBool(mdm.KeyDisableAutoConnect, msg.DisableAutoConnect),
mdm.ConflictBool(mdm.KeyAllowServerSSH, msg.ServerSSHAllowed),
mdm.ConflictBool(mdm.KeyDisableClientRoutes, msg.DisableClientRoutes),
mdm.ConflictBool(mdm.KeyDisableServerRoutes, msg.DisableServerRoutes),
mdm.ConflictBool(mdm.KeyBlockInbound, msg.BlockInbound),
mdm.ConflictInt64(mdm.KeyWireguardPort, msg.WireguardPort),
})
}

View File

@@ -11,37 +11,18 @@ import (
"github.com/netbirdio/netbird/client/internal/daemonaddr"
"github.com/netbirdio/netbird/client/internal/ipcauth"
"github.com/netbirdio/netbird/client/mdm"
"github.com/netbirdio/netbird/client/proto"
)
type MDMFields struct {
ManagementURL string `json:"managementURL"`
PreSharedKey bool `json:"preSharedKey"`
WireguardPort bool `json:"wireguardPort"`
RosenpassEnabled bool `json:"rosenpassEnabled"`
RosenpassPermissive bool `json:"rosenpassPermissive"`
DisableClientRoutes bool `json:"disableClientRoutes"`
DisableServerRoutes bool `json:"disableServerRoutes"`
AllowServerSSH *bool `json:"allowServerSSH"`
DisableAutoConnect bool `json:"disableAutoConnect"`
DisableAutostart bool `json:"disableAutostart"`
BlockInbound bool `json:"blockInbound"`
DisableMetricsCollection bool `json:"disableMetricsCollection"`
SplitTunnelMode bool `json:"splitTunnelMode"`
SplitTunnelApps bool `json:"splitTunnelApps"`
DisableAdvancedView bool `json:"disableAdvancedView"`
}
// MDMFields is the shared per-key MDM enforcement snapshot; see mdm.Fields.
type MDMFields = mdm.Fields
type Features struct {
DisableProfiles bool `json:"disableProfiles"`
DisableNetworks bool `json:"disableNetworks"`
DisableUpdateSettings bool `json:"disableUpdateSettings"`
}
// Features is the shared feature-gate snapshot; see mdm.Features.
type Features = mdm.Features
type Restrictions struct {
MDM MDMFields `json:"mdm"`
Features Features `json:"features"`
}
// Restrictions is the shared UI enforcement snapshot; see mdm.Restrictions.
type Restrictions = mdm.Restrictions
// Privilege tells the frontend whether this process may perform the changes the
// daemon restricts to root/administrator, and carries the command for each so a