From 13d49e7bf941417d84915a3095aba6789db24a47 Mon Sep 17 00:00:00 2001 From: David Brieck Date: Fri, 24 Apr 2026 13:38:51 +0000 Subject: [PATCH 1/2] feat: add MDM managed app configuration support for iOS Integrate Apple Managed App Configuration to allow MDM solutions to push NetBird configuration to managed iOS devices. - Add ManagedConfigReader to read from com.apple.configuration.managed - Apply MDM config in NetworkExtensionAdapter.start() - Apply MDM config in PacketTunnelProvider.startTunnel() - Auto-login with setup key when provided by MDM Related to netbirdio/netbird#1918 --- NetbirdKit/ManagedConfigReader.swift | 149 ++++++++++++++++++ NetbirdKit/NetworkExtensionAdapter.swift | 19 +++ .../PacketTunnelProvider.swift | 12 ++ 3 files changed, 180 insertions(+) create mode 100644 NetbirdKit/ManagedConfigReader.swift diff --git a/NetbirdKit/ManagedConfigReader.swift b/NetbirdKit/ManagedConfigReader.swift new file mode 100644 index 0000000..2447f06 --- /dev/null +++ b/NetbirdKit/ManagedConfigReader.swift @@ -0,0 +1,149 @@ +// +// ManagedConfigReader.swift +// NetBird +// +// Reads MDM-managed app configuration pushed via Apple Managed App Configuration (AppConfig). +// Configuration is delivered through the com.apple.configuration.managed UserDefaults domain +// by MDM solutions such as Microsoft Intune, Jamf Pro, VMware Workspace ONE, or Mosyle. +// +// Key names match those defined in the Go SDK's ManagedConfig constants. +// + +import Foundation +import NetBirdSDK +import os + +/// Reads and applies MDM-managed app configuration from the Apple managed configuration domain. +/// +/// ## How it works +/// - MDM pushes key-value pairs to the `com.apple.configuration.managed` UserDefaults domain +/// - This reader checks that domain for NetBird-specific keys +/// - Values are applied to the Go SDK's config file, overriding user preferences +/// - Setup keys trigger silent device registration without user interaction +/// +/// ## Supported keys +/// - `managementUrl` — Management server URL +/// - `setupKey` — Setup key for silent device registration +/// - `adminUrl` — Admin dashboard URL +/// - `preSharedKey` — WireGuard pre-shared key +/// - `rosenpassEnabled` — Enable Rosenpass post-quantum encryption +/// - `rosenpassPermissive` — Allow non-Rosenpass peers +/// - `disableAutoConnect` — Prevent auto-connect on launch +class ManagedConfigReader { + + private static let logger = Logger(subsystem: "io.netbird.app", category: "ManagedConfigReader") + + /// The Apple-native MDM managed configuration domain + private static let managedDomain = "com.apple.configuration.managed" + + /// Reads managed configuration from the MDM domain. + /// Returns a populated ManagedConfig, or nil if no MDM config is available. + static func read() -> NetBirdSDKManagedConfig? { + guard let managedDefaults = UserDefaults(suiteName: managedDomain) else { + logger.debug("ManagedConfigReader: managed defaults domain not available") + return nil + } + + let dict = managedDefaults.dictionaryRepresentation() + + // Check if any NetBird keys are present + let managementUrlKey = NetBirdSDKGetManagedConfigKeyManagementURL() + let setupKeyKey = NetBirdSDKGetManagedConfigKeySetupKey() + let adminUrlKey = NetBirdSDKGetManagedConfigKeyAdminURL() + let preSharedKeyKey = NetBirdSDKGetManagedConfigKeyPreSharedKey() + let rosenpassEnabledKey = NetBirdSDKGetManagedConfigKeyRosenpassEnabled() + let rosenpassPermissiveKey = NetBirdSDKGetManagedConfigKeyRosenpassPermissive() + let disableAutoConnectKey = NetBirdSDKGetManagedConfigKeyDisableAutoConnect() + + guard let config = NetBirdSDKNewManagedConfig() else { + logger.error("ManagedConfigReader: failed to create ManagedConfig") + return nil + } + + if let managementUrl = dict[managementUrlKey] as? String, !managementUrl.isEmpty { + config.setManagementURL(managementUrl) + logger.info("ManagedConfigReader: management URL configured") + } + + if let setupKey = dict[setupKeyKey] as? String, !setupKey.isEmpty { + config.setSetupKey(setupKey) + // Do not log the setup key value for security + logger.info("ManagedConfigReader: setup key configured") + } + + if let adminUrl = dict[adminUrlKey] as? String, !adminUrl.isEmpty { + config.setAdminURL(adminUrl) + logger.info("ManagedConfigReader: admin URL configured") + } + + if let preSharedKey = dict[preSharedKeyKey] as? String, !preSharedKey.isEmpty { + config.setPreSharedKey(preSharedKey) + logger.info("ManagedConfigReader: pre-shared key configured") + } + + if let rosenpassEnabled = dict[rosenpassEnabledKey] as? Bool { + config.setRosenpassEnabled(rosenpassEnabled) + logger.info("ManagedConfigReader: Rosenpass enabled=\(rosenpassEnabled)") + } + + if let rosenpassPermissive = dict[rosenpassPermissiveKey] as? Bool { + config.setRosenpassPermissive(rosenpassPermissive) + logger.info("ManagedConfigReader: Rosenpass permissive=\(rosenpassPermissive)") + } + + if let disableAutoConnect = dict[disableAutoConnectKey] as? Bool { + config.setDisableAutoConnect(disableAutoConnect) + logger.info("ManagedConfigReader: disable auto-connect=\(disableAutoConnect)") + } + + guard config.hasConfig() else { + logger.debug("ManagedConfigReader: no NetBird keys found in managed config") + return nil + } + + logger.info("ManagedConfigReader: MDM managed configuration loaded successfully") + return config + } + + /// Returns true if any MDM-managed configuration is available. + static func hasManagedConfig() -> Bool { + guard let config = read() else { return false } + return config.hasConfig() + } + + /// Applies MDM config to the config file and optionally performs setup key registration. + /// - Parameters: + /// - configPath: Path to the NetBird config file + /// - deviceName: Device name for registration + /// - Returns: true if MDM config was applied + @discardableResult + static func applyIfAvailable(configPath: String, deviceName: String) -> Bool { + guard let config = read() else { return false } + + do { + try config.apply(configPath) + logger.info("ManagedConfigReader: MDM config applied to \(configPath)") + } catch { + logger.error("ManagedConfigReader: failed to apply MDM config: \(error.localizedDescription)") + return false + } + + // If MDM provides a setup key, attempt silent registration + if config.hasSetupKey() { + do { + guard let auth = NetBirdSDKNewAuth(configPath, "", nil) else { + logger.warning("ManagedConfigReader: failed to create Auth for setup key login") + return true + } + try auth.loginWithSetupKeySync(config.getSetupKey(), deviceName: deviceName) + logger.info("ManagedConfigReader: silent setup key registration completed") + } catch { + // Setup key login may fail if already registered or key expired. + // This is not fatal — continue with normal flow. + logger.warning("ManagedConfigReader: setup key login skipped or failed: \(error.localizedDescription)") + } + } + + return true + } +} diff --git a/NetbirdKit/NetworkExtensionAdapter.swift b/NetbirdKit/NetworkExtensionAdapter.swift index bcdce05..839ab3f 100644 --- a/NetbirdKit/NetworkExtensionAdapter.swift +++ b/NetbirdKit/NetworkExtensionAdapter.swift @@ -84,6 +84,10 @@ public class NetworkExtensionAdapter: ObservableObject { // This must happen in the main app — not via IPC — because the extension // process may not be running yet when start() is called. restoreConfigIfMissing() + + // Apply MDM managed configuration before login. + // MDM values override user-set preferences on every launch. + applyManagedConfig() #endif await loginIfRequired() logger.info("start: loginIfRequired() completed") @@ -93,6 +97,21 @@ public class NetworkExtensionAdapter: ObservableObject { logger.info("start: EXIT") } + #if os(iOS) + /// Reads and applies MDM-managed app configuration if available. + /// MDM config is delivered via the com.apple.configuration.managed UserDefaults domain. + private func applyManagedConfig() { + guard let configPath = Preferences.configFile() else { + logger.warning("applyManagedConfig: config path unavailable") + return + } + let deviceName = Device.getName() + if ManagedConfigReader.applyIfAvailable(configPath: configPath, deviceName: deviceName) { + logger.info("applyManagedConfig: MDM config applied successfully") + } + } + #endif + #if os(iOS) /// If the active profile's config file is missing (deleted after logout) but we have /// a saved management URL, write a minimal config so the SDK uses the correct server diff --git a/NetbirdNetworkExtension/PacketTunnelProvider.swift b/NetbirdNetworkExtension/PacketTunnelProvider.swift index 380ee20..e357eec 100644 --- a/NetbirdNetworkExtension/PacketTunnelProvider.swift +++ b/NetbirdNetworkExtension/PacketTunnelProvider.swift @@ -69,6 +69,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider { return } + // Apply MDM managed configuration at the extension level. + // This ensures MDM values are applied even when the extension starts + // independently (e.g., via On Demand or Always-on VPN). + #if os(iOS) + if let extensionConfigPath = Preferences.configFile() { + let deviceName = Device.getName() + if ManagedConfigReader.applyIfAvailable(configPath: extensionConfigPath, deviceName: deviceName) { + AppLogger.shared.log("PacketTunnelProvider: MDM managed config applied") + } + } + #endif + if adapter.needsLogin() { signalLoginRequired() // Return the error immediately so iOS tears down the tunnel interface at once. From 5e6a59725ca7e70169f6efc13f00d6e2a5a4bf86 Mon Sep 17 00:00:00 2001 From: David Brieck Date: Fri, 24 Apr 2026 15:04:44 +0000 Subject: [PATCH 2/2] fix: pass MDM management URL to NewAuth for setup key login Use config.getManagementURL() so NewAuth connects to the correct MDM-specified server instead of passing an empty string which defaults to api.netbird.io. --- NetbirdKit/ManagedConfigReader.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/NetbirdKit/ManagedConfigReader.swift b/NetbirdKit/ManagedConfigReader.swift index 2447f06..c2632a3 100644 --- a/NetbirdKit/ManagedConfigReader.swift +++ b/NetbirdKit/ManagedConfigReader.swift @@ -128,10 +128,12 @@ class ManagedConfigReader { return false } - // If MDM provides a setup key, attempt silent registration + // If MDM provides a setup key, attempt silent registration. + // Pass the MDM management URL so NewAuth connects to the correct server. if config.hasSetupKey() { + let mgmtUrl = config.getManagementURL() ?? "" do { - guard let auth = NetBirdSDKNewAuth(configPath, "", nil) else { + guard let auth = NetBirdSDKNewAuth(configPath, mgmtUrl, nil) else { logger.warning("ManagedConfigReader: failed to create Auth for setup key login") return true }