From 7715c382ee37aa7f541a1b3eaa4d41d18dc95959 Mon Sep 17 00:00:00 2001 From: riccardom Date: Tue, 16 Jun 2026 12:21:25 +0200 Subject: [PATCH] Adds iOS wiring --- client/ios/NetBirdSDK/client.go | 11 +++++ client/ios/NetBirdSDK/mdm.go | 82 +++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 client/ios/NetBirdSDK/mdm.go diff --git a/client/ios/NetBirdSDK/client.go b/client/ios/NetBirdSDK/client.go index bafbb0031..1acebff3e 100644 --- a/client/ios/NetBirdSDK/client.go +++ b/client/ios/NetBirdSDK/client.go @@ -21,6 +21,7 @@ import ( "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/profilemanager" + "github.com/netbirdio/netbird/client/mdm" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/formatter" "github.com/netbirdio/netbird/route" @@ -78,6 +79,13 @@ type Client struct { connectClient *internal.ConnectClient // preloadedConfig holds config loaded from JSON (used on tvOS where file writes are blocked) preloadedConfig *profilemanager.Config + + // mdmLoader holds the per-Client MDM policy source. Set by + // SetMDMPolicyFetcher (called from the Swift side at extension + // 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 } // NewClient instantiate a new Client @@ -133,6 +141,7 @@ func (c *Client) Run(fd int32, interfaceName string, envList *EnvList) error { if err != nil { return err } + c.applyMDMOverlay(cfg) } c.recorder.UpdateManagementAddress(cfg.ManagementURL.String()) c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive) @@ -258,6 +267,7 @@ func (c *Client) IsLoginRequired() bool { // If we can't load config, assume login is required return true } + c.applyMDMOverlay(cfg) } if cfg == nil { @@ -306,6 +316,7 @@ func (c *Client) LoginForMobile() string { log.Errorf("LoginForMobile: failed to load config: %v", err) return fmt.Sprintf("failed to load config: %v", err) } + c.applyMDMOverlay(cfg) oAuthFlow, err := auth.NewOAuthFlow(ctx, cfg, false, false, "") if err != nil { diff --git a/client/ios/NetBirdSDK/mdm.go b/client/ios/NetBirdSDK/mdm.go new file mode 100644 index 000000000..6f9696b7a --- /dev/null +++ b/client/ios/NetBirdSDK/mdm.go @@ -0,0 +1,82 @@ +//go:build ios + +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 +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. +func (c *Client) SetMDMPolicyFetcher(p PolicyFetcher) { + if p == nil { + c.mdmLoader = mdm.NewLoader(nil) + return + } + 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 / 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()) +}