[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 609 additions and 343 deletions
+52 -6
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
}