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.
Root Cause Observed in NetworkExtension.framework
When 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], NEConfigurationManager invokes:
+[NEConfigurationManager disableConfiguration:otherConfig onDemandOnly:YES]
disableConfiguration:onDemandOnly:YES calls [[otherConfig VPN] setOnDemandEnabled:NO], even when both configurations are NETransparentProxyManager (TunnelType == 2).
Real-World Impact
When two enterprise security products that both use NETransparentProxyManager are installed on the same Mac:
Both configure isEnabled = true, onDemandRules = [NEOnDemandRuleConnect()], and isOnDemandEnabled = true.
Whenever Proxy A saves its preferences, nehelper silently flips Proxy B's isOnDemandEnabled to false (and vice versa).
After a reboot or network transition where Proxy B's initial startVPNTunnel fails while Wi-Fi is still associating and Proxy B relies on VPN On Demand (NEOnDemandRuleConnect) to start its NEAppProxyProvider once network connectivity comes up, macOS never starts Proxy B because its isOnDemandEnabled flag was cleared.
Minimal Reproducer (Swift)
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()Reproduction Output
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=1Questions
Is disableConfiguration:onDemandOnly:YES stripping isOnDemandEnabled across coexisting NETransparentProxyManager (TunnelType = 2) configurations a bug in NEConfigurationManager, or an intentional limitation?
Is there a recommended way for two coexisting NETransparentProxyManager configurations from different vendors to both use On-Demand without disabling each other's isOnDemandEnabled flag on saveToPreferences?
Filed in Feedback Assistant as FB25463155.