How to start AppProxyProvider on macOS

Hello!


I'm trying to create an AppProxy NetworkExtension for macOS, but the code of the extension is not called.

The project is just a simple Hello World app and AppProxy target from Xcode's template.


I believe I have all the formal settings of the project and targets setup correctly:

- The project is builds and runs without any error.

- Calling OSSystemExtensionManager.shared.submitRequest() succeeds.

- systemextensionsctl list shows that the extension is [activated enabled].


Following messages are shown in the Console when filterred by my bundle identifier:

- sysextd Starting enablement of /teamID("") version 1
- sysextd notifying categories that extension  will start
- nesessionmanager System extension  will be started
- sysextd starting extension  via owning category
- nesessionmanager Starting system extension 
- nesessionmanager Submitting launchd job: {
    ...
}
- sysextd Extension point confirmed that extension  is runnable.
- sysextd changing state of extension  to activated_enabled.
- nesessionmanager Adding event subscription  for provider  with extension point com.apple.networkextension.app-proxy


There are NSLog() lines in my AppProxyProvider.swift for rach callback and also in the main.swift of the extension target. None of those logs can be seen via the Console application. The class AppProxyProvider is set as com.apple.networkextension.app-proxy in the Info.plist under NEProviderClasses.


I have successfully built and run the SimpleFirewall example project with my added log lines and that one works correctly.


Isn't there something I forgot to implement so that the code of the AppProxyProvider is actually launched?

App proxy providers are a form of VPN and, as such, are governed by the VPN start rules. VPNs can start in one of three ways:

  • Manually, via Network preferences

  • Programmatically, via the

    NEVPNConnection
    that’s available via the
    connection
    property of the
    NEVPNManager
    [1]
  • Automatically, via VPN On Demand

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

[1] If it’s an app proxy provider, the actual types will be

NETunnelProviderSession
and
NETunnelProviderManager
respectively.

eskimo, thank you for your reply!

I'd like to start it programmatically from my wrapper app, that previously called OSSystemExtensionManager.shared.submitRequest().

I have added following code to successful extension request callback:


let manager = NEAppProxyProviderManager.shared()
let session = manager.connection

if session.status != .connected {
  do {
      try session.startVPNTunnel()
  }
  catch {
      print(error)
  }
}

But the line 6 throws error: Error Domain=NEVPNErrorDomain Code=1 "(null)" which is

configurationInvalid
. The documentation mentions errors in configuration of the extension/app.


Please, what kind of configuration do I need to create and where should I put it in my project? Is there any example of a minimal configuration for an App Proxy Network Extension?

I'd like to start it programmatically from my wrapper app

OK. I expect that’d be feasible but I must admit to have never tried it.

But the line 6 throws …

configurationInvalid
.

Two things:

  • Please confirm that your container app has the NE entitlements. You can do this using:

    $ codesign -d --entitlements :- /Applications/MyApp.app

    .

  • If that’s not the problem, take a look in the system log at the time of the error to see if it has any hints as to what’s gone wrong.

Oh, wait, one more thing: If you try to start your VPN manually from Network preferences, does that work?

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

1.

$ codesign -d --entitlements :- /Applications/MyApp.app

Yes, the container has the NE entitlements.

I also have the developer mode turned on via the utility systemextensionsctl.


2.

In "System Preferences -> Network" I don't see any reference to my network extension or the bundle.


3.

There is one line from the log which looks suspicios, but it's not an error:

sysextd request contains no authorizationref

Hmmm, it sounds like your tunnel configuration isn’t set up correctly. Are you setting this up programmatically? Or via a configuration profile?

I’m going to recommend that you investigate this by switching to an app extension based app proxy provider. That’ll eliminate any potential complications associated with using a system extension. Once you get that working, you can then switch back to using a system extension to see how things work there.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

As I stated previousluy, I don't know what configuration needs to be created/set. So maybe that's the problem, that I don't have any configuration yet. About the configuration: I found that the classes NEAppProxyProviderManager and NETransparentProxyManager have protocolConfiguration. Did you mean this by the configuration?

I have been using and implementing Kernel Network Extensions, specifically socket filters. After reading the documentation I understand the API but I'm still kinda lost how to just run a 'Hello World' App Proxy implementation.

Did you mean this by the configuration?

Yes. An app proxy provider must be configured before you can start it. You can do this is one of two ways:

On iOS, you must use a profile during deployment; programmatic configuration only works during development. I don’t remember if this applies on macOS as well.

Additionally, you must use MDM to assign an app to the app proxy’s tunnel during deployment. During development you can get around this using

NETestAppMapping
. In contrast, on macOS you can take advantage of the
com.apple.vpn.managed.appmapping
payload.

I had a look and, quite surprisingly, I don’t have code for programmatic configuration lying around. I generally test with a configuration profile on the principle of start as you mean to go on.

Having said that, creating this configuration is pretty simple: Create an instance of

NETunnelProviderProtocol
, set the relevant properties, and then set
protocolConfiguration
to that. There’s two gotchas:
  • Always set

    serverAddress
    , even if only to a dummy value. Without this, you won’t be able to save the configuration.
  • On the Mac specifically, make sure to set

    providerBundleIdentifier
    .

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I got inspired byt these forum threads:

https://forums.developer.apple.com/thread/123170

https://forums.developer.apple.com/thread/121823

because it looks like they have similar goal as me.


  1. I switched to the NETransparentProxyManager
  2. set up the NETunnelProviderProtocol with providerBundleIdentifier dummy serverAddress
  3. assigned protocol to the manager, isEnabled to true and set the localizedDesription


saveToPreferences() ended up with the error:

"Failed to save the filter configuration: The operation couldn’t be completed. (NEVPNErrorDomain error 4.)"


According to the documentation

https://developer.apple.com/documentation/networkextension/nevpnerror/code/configurationstale

"This error also occurs if the app tries to save the VPN configuration before loading it from the Network Extension preferences the first time after the app launches."


I modified the code, so that first I try to call NETransparentProxyManager.loadAllFromPreferences(). Then if it ends with an error, then I try to set up the configuration and save it via saveToPreferences(). But that also results in the same error (NEVPNErrorDomain error 4.).


Here is the code snippet:

private func loadProvider() {

    ...

    let activationRequest = OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: extensionIdentifier, queue: .main)
    activationRequest.delegate = self

    // Result is handled in
    // request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result)
    OSSystemExtensionManager.shared.submitRequest(activationRequest)
}


func request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) {

    guard result == .completed else {
        return
    }
    
    loadProviderConfiguration();
}


private func loadProviderConfiguration() {
    
    NETransparentProxyManager.loadAllFromPreferences{ error, arg  in
        if let loadError = error {            

            let proto = NETunnelProviderProtocol()
            proto.serverAddress = "example.com"
            proto.providerBundleIdentifier = Bundle.main.infoDictionary!["CFBundleIdentifier"] as? String
            
            let manager = NETransparentProxyManager.shared()
            manager.protocolConfiguration = proto
            manager.isEnabled = true
            manager.localizedDescription = Bundle.main.infoDictionary!["CFBundleName"] as? String
            
            manager.saveToPreferences(completionHandler: { (saveError:Error?) in
                DispatchQueue.main.async {
                    if let error = saveError {
                        
                        // NOTE: The error is always:
                        // Failed to save the filter configuration: The operation couldn’t be completed. (NEVPNErrorDomain error 4.)
                        return
                    }
                }
            });
        }
    }
}

OK, well, transparent proxy is something I’ve played around with. Pasted in below is the code I use to configure it.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
private func configureTransparentProxy() {
    os_log(.debug, log: self.log, "will load configurations")
    NETransparentProxyManager.loadAllFromPreferences { managersQ, errorQ in
        precondition(Thread.isMainThread)
        if let nsError = errorQ as NSError? {
            os_log(.debug, log: self.log, "did not load configurations, error: %{public}@ / %zd", nsError.domain, nsError.code)
            self.status = "Error loading configurations."
            return
        }
        let managers = managersQ ?? []
        os_log(.debug, log: self.log, "did load configurations, count: %zd", managers.count)

        let managerQ = managers.first(where: Self.isOurManager(_:))
        let manager: NETransparentProxyManager
        if let m = managerQ {
            os_log(.debug, log: self.log, "did find configuration, updating")
            manager = m
        } else {
            os_log(.debug, log: self.log, "did not find configuration, adding")
            manager = NETransparentProxyManager()
        }
        self.storeConfiguration(in: manager)
        manager.saveToPreferences { errorQ in
            if let nsError = errorQ as NSError? {
                os_log(.debug, log: self.log, "did not save configuration, error: %{public}@ / %zd", nsError.domain, nsError.code)
                self.status = "Error saving configuration."
                return
            }
            os_log(.debug, log: self.log, "did save configuration")
            self.status = "Configuration saved."
        }
    }
}

private static func isOurManager(_ manager: NETransparentProxyManager) -> Bool {
    guard
        let proto = manager.protocolConfiguration,
        let tunnelProto = proto as? NETunnelProviderProtocol
    else {
        return false
    }
    return tunnelProto.providerBundleIdentifier == "com.example.apple-samplecode.PassThroughProxy-macOS.TransparentProxy"
}

private func storeConfiguration(in manager: NETransparentProxyManager) {
    let proto = (manager.protocolConfiguration as? NETunnelProviderProtocol) ?? NETunnelProviderProtocol()

    // `NEVPNProtocol` properties…
    //
    // You have to configure a server address even if you don’t use a
    // server.
    proto.serverAddress = "localhost"
    // All the other `NEVPNProtocol` properties are optional.

    // `NETunnelProviderProtocol` properties…
    //
    // Use `providerConfiguration` to pass configuration parameters to your
    // proxy.
    //
    // `providerBundleIdentifier` is important on macOS.
    proto.providerBundleIdentifier = "com.example.apple-samplecode.PassThroughProxy-macOS.TransparentProxy"

    manager.protocolConfiguration = proto
    manager.isEnabled = true
    manager.localizedDescription = "PassThroughProxy"
}

The code works like a charm! It looks like I had the providerBundleIdentifier set wrong. Thank you very much eskimo!

Now I get also the system prompt to allow the VPN and it's listed in the Network Preferences. Also the code from my NetworkExtension's main.swift is executed.


But the startProxy(...) of the extension code still doesn't want to be called.

My provider class is AppProxyProvider: NEAppProxyProvider.

It's listed in the Info.plist as

NetworkExtension

    ...
    NEProviderClasses
    
        com.apple.networkextension.app-proxy
        $(PRODUCT_MODULE_NAME).AppProxyProvider
   


I also added this code right after your line 30.

IPCConnection.shared.register(withExtension: self.extensionBundle, delegate: self) { success in
    DispatchQueue.main.async {
        if success {
            let session = self.myManager!.connection            
            if session.status != .connected {
                do {
                    try session.startVPNTunnel(options: nil)
                }
                catch {
                    print(error)
                }
            }
        }
    }
}


And when startVPNTunnel() is called (line 07) I see this error in Console:

nesessionmanager NEFlowDivertPlugin([inactive]): Sending start command
nesessionmanager [inactive]: starting
nesessionmanager [1318]: Tearing down XPC connection due to setup error: Error Domain=NEAgentErrorDomain Code=2 "(null)"
nesessionmanager NESMTransparentProxySession[Primary Tunnel::>:(null)] in state NESMVPNSessionStateStarting: plugin NEFlowDivertPlugin([inactive]) started with PID 0 error Error Domain=NEAgentErrorDomain Code=2 "(null)"
nesessionmanager [1318]: XPC connection went away
nesessionmanager [1318]: disposing


Lines 03 and 04:

Error Domain=NEAgentErrorDomain Code=2 "(null)"

The same happens if I try to execute Connect the from the Preferences->Network.

It looks like there is no information about this kind of an error even on https://developer.apple.com/documentation/.


Do you know a meaning of the error?

The

NEAgentErrorDomain
error domain isn’t documented but if I’m reading the code correctly error 2 is
NEAgentErrorCancel
, that is, not very helpful.

My first step in debugging stuff like this is to determine whether the provider is ever instantiated. I do this by adding a log point to its initialiser.

final class TransparentProxyProvider: NEAppProxyProvider {

    /// We publish this as a global so that `main` can use it.

    static let log = OSLog(subsystem: "com.example.apple-samplecode.PassThroughProxy", category: "provider")

    override init() {
        self.log = Self.log
        os_log(.debug, log: self.log, "init")
        …
        super.init()
        …
    }

    …
}

For a transparent proxy, which is a system extension, you also want to log from your main function. Here’s what mine looks like:

import NetworkExtension
import os.log

/// The main entry point for the transparent proxy provider system extension.

func main() -> Never {
    let log = TransparentProxyProvider.log
    os_log(.debug, log: log, "will start system extension mode")
    autoreleasepool {
        NEProvider.startSystemExtensionMode()
    }
    os_log(.debug, log: log, "will start dispatch main")
    dispatchMain()
}

main()

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I added logging to init() and main().

According to the logs I see:

start system extension mode
start dispatch main


So no init() is called.


I looked closer at all the logs in Console and there are some additional suspicious lines except the undocumented error:


1. LOGS

default 13:43:00.774746+0100 <my wrapper app>   Current bundle (<path to .app>) does not have a PlugIns directory
default 13:43:00.774761+0100 <my wrapper app>   Did not find any extension in <path to .app> that implements a provider for com.apple.networkextension.app-proxy


I don't understand these line because:

- The project was created via the NetworkExtension target

- the extension is located in <path to my wrapper app>/Contents/Library/SystemExtensions/<BundleIdentifier>.systemextension

- I have developer mode ON via the systemextensionsctl

- the main() of the extension runs

- In Info.plist of the extension I have:

<key>NetworkExtension</key>
<dict>
    <key>NEMachServiceName</key>
    <string>$(TeamIdentifierPrefix).<BundleIdentifier>.<Extension name></string>
    <key>NEProviderClasses</key>
    <dict>
        <key>com.apple.networkextension.app-proxy</key>
        <string>$(PRODUCT_MODULE_NAME).TransparentProxyProvider</string>
    </dict>
</dict>

(I renamed the class to TransparentProxyProvider and its occurence according to your example from previous post)


2. LOGS

default 13:43:00.828876+0100 taskgated-helper Checking against 1 eligible provisioning profiles
default 13:43:00.828943+0100 taskgated-helper Checking profile: Mac Team Provisioning Profile: <BundleIdentifier>
error 13:43:00.828962+0100 taskgated-helper <BundleIdentifier>: Unsatisfied entitlements: com.apple.security.application-groups
error 13:43:00.828974+0100 taskgated-helper Disallowing: <BundleIdentifier>
error 13:43:00.829235+0100 amfid CPValidateProvisioningDictionariesExtViaBridge returned invalid result: {
    success = 0;
}
default 13:43:00.829258+0100 amfid Soft-restriction provisioning profile validation failure: No matching provisioning profile
default 13:43:00.829272+0100 amfid Unsatisfied entitlements key is not type CFString, this should not happen.
default 13:43:00.829283+0100 amfid Provisioning Profile does not provision soft-restricted entitlements.

See lines 03. and 04.

The line 04. looks kind of serious, but everything somehow runs up to the main() in the extension.


In .entitlements for both the wrapper app and the extension I have following:

<key>com.apple.security.application-groups</key>
<array>
    <string>$(TeamIdentifierPrefix).<my team group></string>
</array>

Please drop me a line via email (my address is in my signature). Make sure to reference this thread ’cause I get a lot of email (-:

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you very much for you guidance. Now everything works as it should.


But there is an interesting behaviour with the network extensions, which I spotted also by the SimpleFirewall (content filter) example project.

When the network extension and VPN in the System Preferences are going to be installed clean, (i.e. run systemextensionsctl reset + remove VPN from System Preferences) the following sequence doesn't succeed:

1. Install - request the extension to be installed - finishes OK - the extension is active and enabled

2. Configure - save configuration of the VPN to System Preference - finishes OK - The VPN is disconnected in the System Preferences

3. Connect - call startVPNTunnel() of click Connect in System Preferences - Tries to connect the VPN, but noes not succeed.


I'm searching the logs for some descriptive clue, but found only the "NEAgentErrorDomain Code=2" so far:

default 09:22:55.844285+0100 nesessionmanager com.apple.networkextension NEFlowDivertPlugin([inactive]): Sending start command
error 09:22:55.847394+0100 nesessionmanager com.apple.networkextension Failed to launch 
default 09:22:55.848001+0100 nesessionmanager com.apple.networkextension NESMTransparentProxySession[Primary Tunnel:::(null)] in state NESMVPNSessionStateStarting: plugin NEFlowDivertPlugin([inactive]) started with PID 0 error Error Domain=NEAgentErrorDomain Code=2 "(null)"


But after a second Install -> Configure -> Connect it works normally - connects the VPN and starts the proxy provider.

Hi eldred


I am facing same issue.


Did you get the solution of it?


Thanks

How to start AppProxyProvider on macOS
 
 
Q