On macOS, multiple NETransparentProxyManager (NEAppProxyProvider) configurations from different vendors can be enabled (isEnabled = true) and run simultaneously.
However, we discovered that when one NETransparentProxyManager configuration calls saveToPreferencesWithCompletionHandler: with isOnDemandEnabled = true, /usr/libexec/nehelper (NEConfigurationManager) silently modifies any other enabled NETransparentProxyManager configuration in /Library/Preferences/com.apple.networkextension.plist and flips its OnDemandEnabled to false (and nesessionmanager logs Resetting VPN On Demand and marks the other session not-kernel-activated).
Unlike NEVPNManager.isEnabled (which documents mutual exclusion for personal VPNs), NETransparentProxyManager is designed so multiple transparent proxies can coexist simultaneously, and neither NETransparentProxyManager.h nor NEVPNManager.h documents that isOnDemandEnabled is mutually exclusive across coexisting transparent proxy configurations.
NetworkExtension.frameworkWhen inspecting -[NEConfigurationManager saveConfigurationToDisk:updateSCPreferences:currentSignature:userUUID:notifyNow:isUpgrade:], when iterating over existing NEConfiguration objects of the same grade:
- If
[[newConfig VPN] isEnabled] && [[otherConfig VPN] isEnabled] && [[newConfig VPN] isOnDemandEnabled] && [[otherConfig VPN] isOnDemandEnabled],NEConfigurationManagerinvokes:
+[NEConfigurationManager disableConfiguration:otherConfig onDemandOnly:YES] disableConfiguration:onDemandOnly:YEScalls[[otherConfig VPN] setOnDemandEnabled:NO], even when both configurations areNETransparentProxyManager(TunnelType == 2).
When two enterprise security products that both use NETransparentProxyManager are installed on the same Mac:
- Both configure
isEnabled = true,onDemandRules = [NEOnDemandRuleConnect()], andisOnDemandEnabled = true. - Whenever Proxy A saves its preferences,
nehelpersilently flips Proxy B'sisOnDemandEnabledtofalse(and vice versa). - After a reboot or network transition where Proxy B's initial
startVPNTunnelfails while Wi-Fi is still associating and Proxy B relies on VPN On Demand (NEOnDemandRuleConnect) to start itsNEAppProxyProvideronce network connectivity comes up, macOS never starts Proxy B because itsisOnDemandEnabledflag was cleared.
Compile this minimal CLI inside an app bundle signed with the com.apple.developer.networking.networkextension = ["app-proxy-provider"] entitlement:
import Foundation
import NetworkExtension
let args = CommandLine.arguments
let name = args.count > 1 ? args[1] : "PocProxyA"
let action = args.count > 2 ? args[2] : "status"
DispatchQueue.main.async {
NETransparentProxyManager.loadAllFromPreferences { managers, error in
if let error = error {
print("[\(name)] loadAllFromPreferences error: \(error)")
exit(1)
}
let all = managers ?? []
let existing = all.first(where: { $0.localizedDescription == name })
if action == "status" {
if let mgr = existing {
print("[\(name)] -> isEnabled=\(mgr.isEnabled), isOnDemandEnabled=\(mgr.isOnDemandEnabled), rulesCount=\(mgr.onDemandRules?.count ?? 0)")
} else {
print("[\(name)] -> not found")
}
exit(0)
}
if action == "remove" {
guard let mgr = existing else { exit(0) }
mgr.removeFromPreferences { err in
print("[\(name)] removed (err=\(String(describing: err)))")
exit(0)
}
return
}
// action == "save"
let mgr = existing ?? NETransparentProxyManager()
let proto = (mgr.protocolConfiguration as? NETunnelProviderProtocol) ?? NETunnelProviderProtocol()
proto.providerBundleIdentifier = "com.example.poc.extension"
proto.serverAddress = "127.0.0.1"
mgr.protocolConfiguration = proto
mgr.localizedDescription = name
mgr.isEnabled = true
mgr.onDemandRules = [NEOnDemandRuleConnect()]
mgr.isOnDemandEnabled = true
print("[\(name)] Saving NETransparentProxyManager with isEnabled=true, isOnDemandEnabled=true...")
mgr.saveToPreferences { err in
if let err = err {
print("[\(name)] saveToPreferences error: \(err)")
exit(1)
}
print("[\(name)] saveToPreferences succeeded! isEnabled=\(mgr.isEnabled), isOnDemandEnabled=\(mgr.isOnDemandEnabled)")
exit(0)
}
}
}
RunLoop.main.run()
Running save on PocProxyA, then save on PocProxyB, and checking status on both:
=== Step 1: PocProxyA saves isOnDemandEnabled = true ===
[PocProxyA] Saving NETransparentProxyManager with isEnabled=true, isOnDemandEnabled=true...
[PocProxyA] saveToPreferences succeeded! isEnabled=true, isOnDemandEnabled=true
[PocProxyA] -> isEnabled=true, isOnDemandEnabled=true, rulesCount=1
=== Step 2: PocProxyB saves isOnDemandEnabled = true ===
[PocProxyB] Saving NETransparentProxyManager with isEnabled=true, isOnDemandEnabled=true...
[PocProxyB] saveToPreferences succeeded! isEnabled=true, isOnDemandEnabled=true
=== Step 3: Check BOTH configurations after PocProxyB saved ===
[PocProxyA] -> isEnabled=true, isOnDemandEnabled=false, rulesCount=1
[PocProxyB] -> isEnabled=true, isOnDemandEnabled=true, rulesCount=1
=== Step 4: PocProxyA saves isOnDemandEnabled = true again ===
[PocProxyA] Saving NETransparentProxyManager with isEnabled=true, isOnDemandEnabled=true...
[PocProxyA] saveToPreferences succeeded! isEnabled=true, isOnDemandEnabled=true
=== Step 5: Check BOTH configurations after PocProxyA saved ===
[PocProxyA] -> isEnabled=true, isOnDemandEnabled=true, rulesCount=1
[PocProxyB] -> isEnabled=true, isOnDemandEnabled=false, rulesCount=1
- Is
disableConfiguration:onDemandOnly:YESstrippingisOnDemandEnabledacross coexistingNETransparentProxyManager(TunnelType = 2) configurations a bug inNEConfigurationManager, or an intentional limitation? - Is there a recommended way for two coexisting
NETransparentProxyManagerconfigurations from different vendors to both use On-Demand without disabling each other'sisOnDemandEnabledflag onsaveToPreferences?
Filed in Feedback Assistant as FB25463155.