feat: add MDM managed app configuration support for iOS#105
Conversation
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
📝 WalkthroughWalkthroughThis PR adds MDM (Mobile Device Management) configuration support to NetBird iOS. It introduces a new reader that extracts managed settings from Apple's device configuration domain, applies them to SDK configuration, optionally attempts silent setup-key registration, and integrates the application at both the main app startup and network extension tunnel startup points. ChangesMDM Managed Configuration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
|
Only users with a collaborator, contributor, member, or owner role can interact with CodeRabbit. |
Use config.getManagementURL() so NewAuth connects to the correct MDM-specified server instead of passing an empty string which defaults to api.netbird.io.
|
Update pushed in dbrieck:feat/mdm-managed-config at
Validation:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
NetbirdKit/ManagedConfigReader.swift (1)
131-134: 💤 Low valueSimplify redundant
hasConfig()check.The
read()method already returnsnilwhenconfig.hasConfig()is false (line 121-124). The secondhasConfig()call here is redundant since a non-nil return fromread()guaranteeshasConfig()is true.♻️ Proposed simplification
static func hasManagedConfig() -> Bool { - guard let config = read() else { return false } - return config.hasConfig() + return read() != nil }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@NetbirdKit/ManagedConfigReader.swift` around lines 131 - 134, The hasManagedConfig() implementation should be simplified by removing the redundant config.hasConfig() check because read() already returns nil when hasConfig() is false; change hasManagedConfig() (which currently calls read() and then config.hasConfig()) to simply return whether read() returns non-nil (i.e., replace the current guard/return with a single boolean check based on read()) so only read() is used to determine presence of managed config.NetbirdNetworkExtension/PacketTunnelProvider.swift (1)
72-82: ⚡ Quick winConsider logging when MDM config application is skipped or fails.
When
ManagedConfigReader.applyIfAvailable()returnsfalse(MDM config unavailable or application failed), there's no log message. Adding a log entry for the failure/skip case would improve observability when troubleshooting MDM configuration issues in production.📝 Proposed logging enhancement
`#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") + } else { + AppLogger.shared.log("PacketTunnelProvider: MDM managed config not available or failed to apply") } } `#endif`🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@NetbirdNetworkExtension/PacketTunnelProvider.swift` around lines 72 - 82, The current PacketTunnelProvider startup only logs when ManagedConfigReader.applyIfAvailable(...) returns true; add logging for the skipped/failed cases: when Preferences.configFile() is nil log that no extension config path was found, and when applyIfAvailable returns false log that MDM config was unavailable or failed to apply (include deviceName and extensionConfigPath). Update the block surrounding Preferences.configFile(), Device.getName(), and ManagedConfigReader.applyIfAvailable(...) to emit these failure/skip logs via AppLogger.shared.log so you can observe both missing config path and unsuccessful application attempts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@NetbirdNetworkExtension/PacketTunnelProvider.swift`:
- Around line 72-82: The PacketTunnelProvider.startTunnel block currently calls
ManagedConfigReader.applyIfAvailable with Preferences.configFile(); change it to
use the adapter's initialized config path (adapter.initializedConfigPath) or the
startVPNTunnel options-derived configPath that was used to create the adapter so
MDM is applied to the same profile the adapter will read (locate the code around
PacketTunnelProvider.startTunnel and the Adapter/adapter initialization path).
Also add a log path when ManagedConfigReader.applyIfAvailable(...) returns false
so failures to find/apply managed config are visible (use AppLogger.shared.log
or AppLogger.shared.error in the same conditional).
---
Nitpick comments:
In `@NetbirdKit/ManagedConfigReader.swift`:
- Around line 131-134: The hasManagedConfig() implementation should be
simplified by removing the redundant config.hasConfig() check because read()
already returns nil when hasConfig() is false; change hasManagedConfig() (which
currently calls read() and then config.hasConfig()) to simply return whether
read() returns non-nil (i.e., replace the current guard/return with a single
boolean check based on read()) so only read() is used to determine presence of
managed config.
In `@NetbirdNetworkExtension/PacketTunnelProvider.swift`:
- Around line 72-82: The current PacketTunnelProvider startup only logs when
ManagedConfigReader.applyIfAvailable(...) returns true; add logging for the
skipped/failed cases: when Preferences.configFile() is nil log that no extension
config path was found, and when applyIfAvailable returns false log that MDM
config was unavailable or failed to apply (include deviceName and
extensionConfigPath). Update the block surrounding Preferences.configFile(),
Device.getName(), and ManagedConfigReader.applyIfAvailable(...) to emit these
failure/skip logs via AppLogger.shared.log so you can observe both missing
config path and unsuccessful application attempts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: fc692422-3b36-41dc-8e81-6df79391e5c1
📒 Files selected for processing (5)
NetBird.xcodeproj/project.pbxprojNetBird/Source/App/NetBirdApp.swiftNetbirdKit/ManagedConfigReader.swiftNetbirdKit/NetworkExtensionAdapter.swiftNetbirdNetworkExtension/PacketTunnelProvider.swift
| // 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 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify NetBirdAdapter exposes initializedConfigPath and check for consistent usage
# Check if NetBirdAdapter has initializedConfigPath property
echo "=== Checking NetBirdAdapter.initializedConfigPath ==="
rg -n -A2 -B2 'var initializedConfigPath' --type=swift
# Find all usages of Preferences.configFile() to check for similar pattern issues
echo -e "\n=== All usages of Preferences.configFile() ==="
rg -n -C3 'Preferences\.configFile\(\)' --type=swift
# Check if there are other places applying MDM config
echo -e "\n=== All ManagedConfigReader.applyIfAvailable call sites ==="
rg -n -C5 'ManagedConfigReader\.applyIfAvailable' --type=swiftRepository: netbirdio/ios-client
Length of output: 14124
🏁 Script executed:
#!/bin/bash
# Inspect the adapter initialization path in PacketTunnelProvider.swift
echo "=== PacketTunnelProvider.swift: startTunnel adapter init context (lines ~30-110) ==="
sed -n '1,200p' NetbirdNetworkExtension/PacketTunnelProvider.swift | nl -ba | sed -n '30,120p'
# Inspect NetBirdAdapter.swift around initializedConfigPath definition and init logic
echo -e "\n=== NetBirdAdapter.swift: initializedConfigPath + init implementation (lines ~230-360) ==="
sed -n '220,360p' NetbirdNetworkExtension/NetBirdAdapter.swift | nl -ba
# Find where adapter is created in PacketTunnelProvider.swift (search for NetBirdAdapter(...))
echo -e "\n=== PacketTunnelProvider.swift: NetBirdAdapter creation ==="
rg -n "NetBirdAdapter\\(" NetbirdNetworkExtension/PacketTunnelProvider.swiftRepository: netbirdio/ios-client
Length of output: 686
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== PacketTunnelProvider.swift: startTunnel context (around adapter creation) ==="
sed -n '1,230p' NetbirdNetworkExtension/PacketTunnelProvider.swift | cat -n | sed -n '20,220p'
echo -e "\n=== PacketTunnelProvider.swift: second adapter re-init context (around line ~160-220) ==="
sed -n '130,230p' NetbirdNetworkExtension/PacketTunnelProvider.swift | cat -n | sed -n '130,230p'
echo -e "\n=== NetBirdAdapter.swift: initializedConfigPath definition + init (around ~230-340) ==="
sed -n '220,360p' NetbirdNetworkExtension/NetBirdAdapter.swift | cat -n | sed -n '220,360p'
echo -e "\n=== NetBirdAdapter.swift: init sets configPath/statePath (search for initializedConfigPath assignment) ==="
rg -n "initializedConfigPath" NetbirdNetworkExtension/NetBirdAdapter.swift
echo -e "\n=== NetBirdAdapter.swift: init signature and how configPath is chosen ==="
sed -n '240,320p' NetbirdNetworkExtension/NetBirdAdapter.swift | cat -n
echo -e "\n=== PacketTunnelProvider.swift: current MDM block lines 60-95 ==="
sed -n '50,110p' NetbirdNetworkExtension/PacketTunnelProvider.swift | cat -nRepository: netbirdio/ios-client
Length of output: 18715
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Find where startVPNTunnel passes configPath/statePath options (iOS app side) ==="
rg -n "startVPNTunnel\\(" -S --type=swift
echo -e "\n=== Find ManagedConfigReader implementation ==="
fd -a "ManagedConfigReader.swift" . 2>/dev/null || true
fd -a "ManagedConfigReader" . 2>/dev/null || true
rg -n "struct ManagedConfigReader|class ManagedConfigReader|ManagedConfigReader\\.applyIfAvailable" --type=swift
echo -e "\n=== Show ManagedConfigReader.applyIfAvailable implementation ==="
rg -n "applyIfAvailable\\(" --type=swift NetbirdKit NetbirdNetworkExtension . || trueRepository: netbirdio/ios-client
Length of output: 2342
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== NetbirdKit/ManagedConfigReader.swift: applyIfAvailable (around ~120-220) ==="
sed -n '1,240p' NetbirdKit/ManagedConfigReader.swift | cat -n
echo -e "\n=== PacketTunnelProvider.swift: adapter options configPath/statePath extraction (lines ~30-60) ==="
sed -n '30,90p' NetbirdNetworkExtension/PacketTunnelProvider.swift | cat -nRepository: netbirdio/ios-client
Length of output: 12429
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== NetworkExtensionAdapter.swift: options passed to startVPNTunnel ==="
sed -n '430,560p' NetbirdKit/NetworkExtensionAdapter.swift | cat -n
echo -e "\n=== Preferences.configFile() implementation ==="
rg -n "func configFile\\(" -S --type=swift NetbirdKit NetbirdNetworkExtension Netbird/NetBird* . || true
# Show the file(s) containing configFile()
files=$(rg -l "configFile\\(" --type=swift NetbirdKit NetbirdNetworkExtension NetBird/Source . || true)
echo -e "\n=== Files mentioning Preferences.configFile() ==="
echo "$files" | head -n 50
if [ -n "${files:-}" ]; then
for f in $(echo "$files" | head -n 10); do
echo -e "\n--- $f (around configFile) ---"
rg -n "configFile\\(" "$f" --type=swift
# Print a small window around the first hit
line=$(rg -n "configFile\\(" "$f" --type=swift | head -n1 | cut -d: -f1)
start=$((line-20)); end=$((line+40))
sed -n "${start},${end}p" "$f" | cat -n
done
fiRepository: netbirdio/ios-client
Length of output: 41085
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== NetbirdKit/Preferences.swift: configFile() implementation (around ~60-120) ==="
sed -n '50,130p' NetbirdKit/Preferences.swift | cat -n
echo -e "\n=== NetbirdKit/ProfileManager.swift: activeConfigPath()/default config path derivation (around migrateIfNeeded references) ==="
rg -n "activeConfigPath|active.*configPath|default.*configPath|migrateIfNeeded\\(" NetbirdKit/ProfileManager.swift NetbirdKit -S --type=swift
sed -n '240,380p' NetbirdKit/ProfileManager.swift | cat -nRepository: netbirdio/ios-client
Length of output: 11984
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== NetBirdAdapter.needsLogin() implementation ==="
rg -n "func needsLogin\\(" NetbirdNetworkExtension/NetBirdAdapter.swift --type=swift
# Print around the definition (best-effort line window)
line=$(rg -n "func needsLogin\\(" NetbirdNetworkExtension/NetBirdAdapter.swift --type=swift | head -n1 | cut -d: -f1)
start=$((line-40)); end=$((line+80))
sed -n "${start},${end}p" NetbirdNetworkExtension/NetBirdAdapter.swift | cat -nRepository: 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 via Preferences.configFile() (profile-aware via ProfileManager.shared.activeConfigPath()), while the adapter is (re)created using the startVPNTunnel options configPath and stores that in adapter.initializedConfigPath. If the active profile changes between startVPNTunnel(options:) and startTunnel, 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-derived configPath) as the configPath passed to ManagedConfigReader.applyIfAvailable(...).
Also consider logging when applyIfAvailable(...) returns false for easier diagnosis when no managed config is present.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@NetbirdNetworkExtension/PacketTunnelProvider.swift` around lines 72 - 82, The
PacketTunnelProvider.startTunnel block currently calls
ManagedConfigReader.applyIfAvailable with Preferences.configFile(); change it to
use the adapter's initialized config path (adapter.initializedConfigPath) or the
startVPNTunnel options-derived configPath that was used to create the adapter so
MDM is applied to the same profile the adapter will read (locate the code around
PacketTunnelProvider.startTunnel and the Adapter/adapter initialization path).
Also add a log path when ManagedConfigReader.applyIfAvailable(...) returns false
so failures to find/apply managed config are visible (use AppLogger.shared.log
or AppLogger.shared.error in the same conditional).
Summary
Add Apple Managed App Configuration support, allowing MDM solutions (Jamf, Intune, Mosyle, etc.) to push NetBird configuration to managed iOS devices for zero-touch deployment.
Related to netbirdio/netbird#1918
Changes
NetbirdKit/ManagedConfigReader.swift— Reads MDM config fromUserDefaults(suiteName: "com.apple.configuration.managed"), populates Go SDKManagedConfig, and handles setup key auto-loginNetbirdKit/NetworkExtensionAdapter.swift— CallsapplyManagedConfig()instart()before login check (iOS only)NetbirdNetworkExtension/PacketTunnelProvider.swift— Applies MDM config instartTunnel()before VPN connection (iOS only)Supported MDM Keys
managementUrlsetupKeyadminUrlpreSharedKeyrosenpassEnabledrosenpassPermissivedisableAutoConnectHow It Works
ManagedConfigReaderreads from managed UserDefaultsDependencies
Summary by CodeRabbit