Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 12 additions & 0 deletions client/android/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"slices"
"strings"
"sync"
"sync/atomic"
"time"

"golang.org/x/exp/maps"
Expand Down Expand Up @@ -90,6 +91,14 @@ type Client struct {
connectClient *internal.ConnectClient
config *profilemanager.Config
cacheDir string

// mdmSource holds the per-Client MDM policy source and its change
// detector as one unit. Set by SetMDMPolicyFetcher (called from the
// Kotlin side). Each Run passes the loader to the resolved Config so
// applyMDMPolicy picks up the active overlay. Nil means "MDM
// enforcement off for this Client".
mdmSource atomic.Pointer[mdmSource]

// Identifies the running profile for the SSO login hint; see profile_state.go.
cfgPath string

Expand Down Expand Up @@ -178,6 +187,7 @@ func (c *Client) Run(platformFiles PlatformFiles, urlOpener URLOpener, isAndroid
if err != nil {
return err
}
c.applyMDMOverlay(cfg)
Comment thread
pappz marked this conversation as resolved.
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive)

Expand Down Expand Up @@ -229,6 +239,7 @@ func (c *Client) RunWithoutLogin(platformFiles PlatformFiles, dns *DNSList, dnsR
if err != nil {
return err
}
c.applyMDMOverlay(cfg)
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive)

Expand Down Expand Up @@ -327,6 +338,7 @@ func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool, anonym
if err != nil {
return "", fmt.Errorf("load config: %w", err)
}
c.applyMDMOverlay(cfg)
cacheDir = platformFiles.CacheDir()
}

Expand Down
52 changes: 52 additions & 0 deletions client/android/client_mdm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//go:build android

package android

import (
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
)

type mdmSource struct {
loader *mdm.Loader
detector *mdm.ChangeDetector
}

// SetMDMPolicyFetcher registers the native-provided MDM policy fetcher on
// this Client; passing nil disables MDM enforcement.
func (c *Client) SetMDMPolicyFetcher(p PolicyFetcher) {
loader := loaderFor(p)
c.mdmSource.Store(&mdmSource{loader: loader, detector: mdm.NewChangeDetector(loader)})
}

// 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 {
src := c.mdmSource.Load()
if src == nil {
return false
}
return src.detector.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) {
loader := c.mdmLoader()
if cfg == nil || loader == nil {
return
}
cfg.ApplyMDMPolicy(loader.Load())
}

func (c *Client) mdmLoader() *mdm.Loader {
if src := c.mdmSource.Load(); src != nil {
return src.loader
}
return nil
}
33 changes: 17 additions & 16 deletions client/android/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/mobile"
"github.com/netbirdio/netbird/client/system"
)
Expand Down Expand Up @@ -46,16 +47,24 @@ 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) {
inputCfg := profilemanager.ConfigInput{
ConfigPath: cfgPath,
ManagementURL: mgmURL,
//
// Auth is constructed under the active MDM policy: the policy is overlaid on
// the resolved config so the login runs against the enforced values, while
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
// the persisted config keeps the caller-supplied ones; a caller-supplied
// management URL is ignored while MDM manages that key. A nil fetcher
// disables MDM enforcement.
func NewAuth(cfgPath string, mgmURL string, fetcher PolicyFetcher) (*Auth, error) {
policy := loaderFor(fetcher).Load()
inputCfg := profilemanager.ConfigInput{ConfigPath: cfgPath}
if _, managed := policy.GetString(mdm.KeyManagementURL); !managed {
inputCfg.ManagementURL = mgmURL
}

cfg, err := profilemanager.UpdateOrCreateConfig(inputCfg)
if err != nil {
return nil, err
}
cfg.ApplyMDMPolicy(policy)

return &Auth{
ctx: context.Background(),
Expand All @@ -75,9 +84,7 @@ func NewAuthWithConfig(ctx context.Context, config *profilemanager.Config, cfgPa
}
}

// SaveConfigIfSSOSupported test the connectivity with the management server by retrieving the server device flow info.
// If it returns a flow info than save the configuration and return true. If it gets a codes.NotFound, it means that SSO
// is not supported and returns false without saving the configuration. For other errors return false.
// SaveConfigIfSSOSupported reports whether the management server supports SSO; the config is already persisted by NewAuth.
func (a *Auth) SaveConfigIfSSOSupported(listener SSOListener) {
go func() {
sso, err := a.saveConfigIfSSOSupported()
Expand All @@ -101,15 +108,10 @@ func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
return false, fmt.Errorf("failed to check SSO support: %v", err)
}

if !supportsSSO {
return false, nil
}

err = profilemanager.WriteOutConfig(a.cfgPath, a.config)
return true, err
return supportsSSO, nil
}

// LoginWithSetupKeyAndSaveConfig test the connectivity with the management server with the setup key.
// LoginWithSetupKeyAndSaveConfig registers the peer with the setup key; the config is already persisted by NewAuth.
func (a *Auth) LoginWithSetupKeyAndSaveConfig(resultListener ErrListener, setupKey string, deviceName string) {
go func() {
err := a.loginWithSetupKeyAndSaveConfig(setupKey, deviceName)
Expand All @@ -134,8 +136,7 @@ func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string
if err != nil {
return fmt.Errorf("login failed: %v", err)
}

return profilemanager.WriteOutConfig(a.cfgPath, a.config)
return nil
}

// Login try register the client on the server
Expand Down
6 changes: 3 additions & 3 deletions client/android/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ 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)
}
if first.config.PrivateKey == "" {
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)
}
Expand All @@ -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)
}
Expand Down
19 changes: 19 additions & 0 deletions client/android/mdm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package android

import (
"github.com/netbirdio/netbird/client/mdm"
)

// 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
}

func loaderFor(p PolicyFetcher) *mdm.Loader {
if p == nil {
return mdm.NewJSONLoader(nil)
}
return mdm.NewJSONLoader(p.FetchJSON)
}
Loading
Loading