Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
db2c9b6
MDM Android mobile wiring
riccardomanfrin Jun 15, 2026
bec26d5
Removes dead code
riccardomanfrin Jun 15, 2026
8741954
Removes static vars
riccardomanfrin Jun 15, 2026
0340893
Now we need to apply MDM in the GetConfig
riccardomanfrin Jun 15, 2026
b2c5732
You now need to explicitly call these around
riccardomanfrin Jun 15, 2026
7715c38
Adds iOS wiring
riccardomanfrin Jun 16, 2026
e1ffb16
Merge branch 'main' into mdm_integration (with unresolved conflict ma…
riccardomanfrin Jul 28, 2026
e92aa7d
Resolve merge conflicts from main
riccardomanfrin Jul 28, 2026
f91f9fc
Convey MDM overlay config to Debug Bundle output
riccardomanfrin Jul 28, 2026
76fd144
Merge remote-tracking branch 'origin/main' into mdm_integration
riccardomanfrin Aug 7, 2026
982626c
Solved conflict in client.go
riccardomanfrin Aug 7, 2026
faa1962
Fixup helper withMDMPolicy -> configWithMDM
riccardomanfrin Aug 7, 2026
e4d4b44
Fixup after merge
riccardomanfrin Aug 7, 2026
12789d1
Merge branch 'main' into mdm_integration
riccardomanfrin Aug 25, 2026
c41d439
Resolve merge conflicts
riccardomanfrin Aug 25, 2026
c281b15
[client] Move MDM enforcement logic into a shared Go layer (#7319)
pappz Aug 26, 2026
ec4b99b
Merge branch 'main' into mdm_integration
pappz Aug 26, 2026
116e1d3
Merge branch 'main' into mdm_integration
riccardomanfrin Sep 4, 2026
5d15ac0
[client] Resolve the main merge conflicts in the MDM integration
riccardomanfrin Sep 4, 2026
d96d22a
[client] Reject explicit empty PSK when MDM enforces a pre-shared key
pappz Sep 4, 2026
47238e0
[client] Apply MDM overlay on the preloaded iOS config in Run
pappz Sep 4, 2026
42f0a63
[client] Gate non-active profile logout behind the MDM profiles switch
pappz Sep 4, 2026
3234ce7
[client] Resolve the managed management URL through the MDM overlay o…
pappz Sep 4, 2026
047715a
[client] Clean up MDM review leftovers
pappz Sep 4, 2026
c1e2e20
[client] Check remote jobs and local metrics keys in the mobile MDM c…
pappz Sep 4, 2026
a52c7ba
[client] Silence the deprecated PreSharedKey lint in the login confli…
pappz Sep 4, 2026
86d6f7d
[client] Publish the mobile MDM loader and detector atomically
pappz Sep 4, 2026
a2b1c99
[client] Report the MDM-overlaid remote jobs value from mobile Prefer…
pappz Sep 4, 2026
1b0dd66
[client] Stop persisting the MDM-overlaid config after mobile logins
pappz Sep 7, 2026
ca548ea
[client] Document that the MDM overlay on Config is not reversible
pappz Sep 7, 2026
cb5efcd
[client] Re-resolve the tvOS preloaded config before every MDM overlay
pappz Sep 7, 2026
c019700
[client] Serve the MDM-managed management URL without touching the co…
pappz Sep 7, 2026
204360e
[client] Align the MDM loader docs with the fetcher precedence and ma…
pappz Sep 7, 2026
9364512
[client] Clean up MDM review nits
pappz Sep 7, 2026
b65f493
[client] Fix int policy values coercing to bool in the MDM test helper
pappz Sep 8, 2026
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
79 changes: 79 additions & 0 deletions client/android/mdm.go
Original file line number Diff line number Diff line change
@@ -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()
}
44 changes: 36 additions & 8 deletions client/mdm/policy_mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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".
// 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) {
//nolint:nilnil // (nil, nil) is the documented platform-absent sentinel; see LoadPolicy.
return nil, nil
if fetcher == nil {
//nolint:nilnil // (nil, nil) is the documented platform-absent sentinel; see LoadPolicy.
return nil, nil
}
return fetcher.Fetch(), nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
Loading