MDM Android mobile wiring

This commit is contained in:
riccardom
2026-06-15 12:04:26 +02:00
parent cd777395f2
commit db2c9b6f49
2 changed files with 116 additions and 9 deletions

79
client/android/mdm.go Normal file
View File

@@ -0,0 +1,79 @@
//go:build android
package android
import (
"encoding/json"
log "github.com/sirupsen/logrus"
"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 via SetMobilePolicyFetcher at app start. Every
// invocation 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.LoadPolicy.
//
// 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
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
}
// SetMobilePolicyFetcher registers the native-provided MDM policy
// fetcher. Call exactly once from the gomobile-init code (Kotlin
// Application.onCreate) before the daemon starts. Passing nil
// effectively disables MDM enforcement on this build.
func SetMobilePolicyFetcher(p PolicyFetcher) {
if p == nil {
mdm.SetMobilePolicyFetcher(nil)
return
}
mdm.SetMobilePolicyFetcher(&jsonFetcherAdapter{inner: p})
}
// OnMDMPolicyChanged is the mobile entry point the native layer calls
// when the OS broadcasts a managed-config change
// (ACTION_APPLICATION_RESTRICTIONS_CHANGED on Android,
// UserDefaults.didChangeNotification on iOS). The hook cancels the
// current engine context so the current Run() returns; the native
// loop is expected to re-invoke Run() afterwards, which re-reads
// the config (including the fresh MDM overlay via the registered
// PolicyFetcher).
//
// No payload is passed — the OS notification only signals "something
// changed"; the actual values are read on-demand by the next
// loadPlatformPolicy call inside Config.apply.
func (c *Client) OnMDMPolicyChanged() {
log.Info("MDM policy change signaled by native; stopping engine to restart with fresh config")
c.Stop()
}

View File

@@ -2,13 +2,41 @@
package mdm
// loadPlatformPolicy is unused on mobile: the native layer (Swift on iOS,
// Kotlin/Java on Android) reads the OS managed-config store and pushes the
// resulting dictionary in-process via a gomobile entry point that lands in
// Phase 5 / Phase 6. The stub keeps the package compilable for mobile
// builds and returns (nil, nil) — the platform-absent sentinel that
// LoadPolicy in policy.go treats as "no MDM source present".
func loadPlatformPolicy() (map[string]any, error) {
//nolint:nilnil // (nil, nil) is the documented platform-absent sentinel; see LoadPolicy.
return nil, nil
// PolicyFetcher is the bridge between Go and the mobile native layer
// (Kotlin/Java on Android, Swift on iOS). The native layer registers
// an implementation at gomobile init via SetMobilePolicyFetcher;
// thereafter every call to loadPlatformPolicy delegates to the
// registered fetcher, which reads the OS-native managed-config store
// (RestrictionsManager on Android, com.apple.configuration.managed
// UserDefaults on iOS) and returns the current snapshot.
//
// Set-once at init, never mutated at runtime → no synchronisation
// required for the read path. The native layer must register before
// any Go code starts polling or processing MDM events.
type PolicyFetcher interface {
Fetch() map[string]any
}
var fetcher PolicyFetcher
// SetMobilePolicyFetcher registers the native-provided fetcher. Call
// exactly once from the gomobile init code (Kotlin Application.onCreate
// / Swift AppDelegate) before the daemon starts. Passing nil disables
// MDM enforcement on this build (loadPlatformPolicy returns
// (nil, nil) — the platform-absent sentinel that LoadPolicy treats as
// "no MDM source present").
func SetMobilePolicyFetcher(p PolicyFetcher) {
fetcher = p
}
// loadPlatformPolicy delegates to the native-provided fetcher. Returns
// (nil, nil) — the platform-absent sentinel — when no fetcher has been
// registered yet, so the package behaves identically to a desktop
// device without an MDM source.
func loadPlatformPolicy() (map[string]any, error) {
if fetcher == nil {
//nolint:nilnil // (nil, nil) is the documented platform-absent sentinel; see LoadPolicy.
return nil, nil
}
return fetcher.Fetch(), nil
}