Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
151 changes: 151 additions & 0 deletions NetbirdKit/ManagedConfigReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//
// 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.
// 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.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
}
}
19 changes: 19 additions & 0 deletions NetbirdKit/NetworkExtensionAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions NetbirdNetworkExtension/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down