-
Notifications
You must be signed in to change notification settings - Fork 25
feat: add MDM managed app configuration support for iOS #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dbrieck
wants to merge
3
commits into
netbirdio:main
Choose a base branch
from
dbrieck:feat/mdm-managed-config
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| // | ||
| // 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 key | ||
| // 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 key | ||
| /// - This reader checks that dictionary 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 key in UserDefaults.standard. | ||
| private static let managedConfigurationKey = "com.apple.configuration.managed" | ||
|
|
||
| private static func managedConfigurationDictionary() -> [String: Any]? { | ||
| if let dict = UserDefaults.standard.dictionary(forKey: managedConfigurationKey) { | ||
| return dict | ||
| } | ||
|
|
||
| if let value = UserDefaults.standard.object(forKey: managedConfigurationKey) { | ||
| logger.warning("ManagedConfigReader: managed configuration value has unsupported root type: \(String(describing: type(of: value)), privacy: .public)") | ||
| } else { | ||
| logger.debug("ManagedConfigReader: managed configuration dictionary not available") | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| private static func managedStringValue(from dict: [String: Any], key: String) -> String? { | ||
| guard let rawValue = dict[key] as? String else { return nil } | ||
| let trimmedValue = rawValue.trimmingCharacters(in: .whitespacesAndNewlines) | ||
|
|
||
| if rawValue != trimmedValue { | ||
| logger.info("ManagedConfigReader: trimmed whitespace from MDM string value for key \(key, privacy: .public)") | ||
| } | ||
|
|
||
| return trimmedValue.isEmpty ? nil : trimmedValue | ||
| } | ||
|
|
||
| /// 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 dict = managedConfigurationDictionary() else { | ||
| return nil | ||
| } | ||
|
|
||
| // 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 = managedStringValue(from: dict, key: managementUrlKey) { | ||
| config.setManagementURL(managementUrl) | ||
| logger.info("ManagedConfigReader: management URL configured") | ||
| } | ||
|
|
||
| if let setupKey = managedStringValue(from: dict, key: setupKeyKey) { | ||
| config.setSetupKey(setupKey) | ||
| // Do not log the setup key value for security | ||
| logger.info("ManagedConfigReader: setup key configured") | ||
| } | ||
|
|
||
| if let adminUrl = managedStringValue(from: dict, key: adminUrlKey) { | ||
| config.setAdminURL(adminUrl) | ||
| logger.info("ManagedConfigReader: admin URL configured") | ||
| } | ||
|
|
||
| if let preSharedKey = managedStringValue(from: dict, key: preSharedKeyKey) { | ||
| 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. | ||
| // 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, mgmtUrl, nil) else { | ||
| logger.warning("ManagedConfigReader: failed to create Auth for setup key login") | ||
| return true | ||
| } | ||
| try auth.login(withSetupKeySync: 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 | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: netbirdio/ios-client
Length of output: 14124
🏁 Script executed:
Repository: netbirdio/ios-client
Length of output: 686
🏁 Script executed:
Repository: netbirdio/ios-client
Length of output: 18715
🏁 Script executed:
Repository: netbirdio/ios-client
Length of output: 2342
🏁 Script executed:
Repository: netbirdio/ios-client
Length of output: 12429
🏁 Script executed:
Repository: netbirdio/ios-client
Length of output: 41085
🏁 Script executed:
Repository: netbirdio/ios-client
Length of output: 11984
🏁 Script executed:
Repository: netbirdio/ios-client
Length of output: 5728
Apply MDM managed config using the adapter’s initialized config path, not
Preferences.configFile()PacketTunnelProvider.startTunnel(lines 72-82) applies MDM viaPreferences.configFile()(profile-aware viaProfileManager.shared.activeConfigPath()), while the adapter is (re)created using thestartVPNTunneloptionsconfigPathand stores that inadapter.initializedConfigPath. If the active profile changes betweenstartVPNTunnel(options:)andstartTunnel, MDM settings (management URL / setup key, etc.) can be written to a different profile’s config than the adapter will read.Use
adapter.initializedConfigPath(or the options-derivedconfigPath) as theconfigPathpassed toManagedConfigReader.applyIfAvailable(...).Also consider logging when
applyIfAvailable(...)returnsfalsefor easier diagnosis when no managed config is present.🤖 Prompt for AI Agents