Interacting with NEPacketTunnelProvider from Login Item / Agent app

Background

I'm working on an App that can manage its own VPN tunnel. The app uses a sysex Network Extension for Developer ID builds and an apex for App Store builds.

I'm observing the VPN's status through NEVPNStatusDidChangeNotification notifications, and starting and stopping it directly from the main App through a NETunnelProviderManager.

The next step is to create a status bar menu within a Login Item / Agent app, so that we have a VPN menu that stays visible even if our main app is closed.

The documentation I'm reading seems to point to the fact that I can only observe NEVPNStatusDidChangeNotification notifications and start / stop the VPN from the main App.

My not-so-ideal solution

For observing VPN status changes I'm considering posting distributed notifications from my NEPacketTunnelProvider.

For controlling the VPN I'm considering launching a hidden copy of my main app using NSWorkspace.shared.open.

Question

Neither of these look like clean approaches to me, so I'm wondering if there's a recommended approach for what I'm trying to do.

Additional notes

  • I considered having the Login Item own the VPN and using XPC to let the main app access it, but if the user decides to turn off my login item, the main app would be unable to interact with the VPN.
  • I considered doing it the other way around, but my login item can't rely on the main app being open at all times.

Accepted Reply

Yeah, there isn’t a good solution to this. I’ve seen it come up before on iOS (somewhere on DevForums there’s a thread about controlling VPN from an old school widget) and the developer filed a bug about it at the time (r. 25869716) but I’m not sure if that fixed things. Regardless, this is even trickier on macOS, where the system allows a lot more flexibility on how programs interact.

On the status front, I think your best option is to provide an XPC service from your sysex that vends the status. You’ll be able to connect to that from anywhere.

On the start/stop front, I don’t see a good solution. Launching your container app hidden might work but I can see that ending up being quite brittle.

However this pans out, I think you should file an enhancement request for a better solution. I could imagine, say, a way to associate a VPN configuration with an app group so that any app entitlement to use that group could control it.

Please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Replies

Yeah, there isn’t a good solution to this. I’ve seen it come up before on iOS (somewhere on DevForums there’s a thread about controlling VPN from an old school widget) and the developer filed a bug about it at the time (r. 25869716) but I’m not sure if that fixed things. Regardless, this is even trickier on macOS, where the system allows a lot more flexibility on how programs interact.

On the status front, I think your best option is to provide an XPC service from your sysex that vends the status. You’ll be able to connect to that from anywhere.

On the start/stop front, I don’t see a good solution. Launching your container app hidden might work but I can see that ending up being quite brittle.

However this pans out, I think you should file an enhancement request for a better solution. I could imagine, say, a way to associate a VPN configuration with an app group so that any app entitlement to use that group could control it.

Please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Enhancement request filed: FB12083539

On the status front, I think your best option is to provide an XPC service from your sysex that vends the status. You’ll be able to connect to that from anywhere.

The only issue with that approach is I don't think it's doable from the appex, so I'll anyways need an alternative. Is my understanding correct?

On the start/stop front, I don’t see a good solution. Launching your container app hidden might work but I can see that ending up being quite brittle.

It seems to work well with a Developer ID app since I can open it using NSWorkspace.shared.open and passing startup arguments that prevent the app from fully launching.

The problem though is that those launch arguments are ignored for the Sandbox version, so I'm trying to figure out an alternative for it.

Any ideas are welcome.

The only issue with that approach is I don't think it's doable from the appex

Kinda. You certainly can’t use XPC. The other ‘obvious’ alternative, NE app messaging, is only available to the container app. However, your appex and other components can share state via an app group and you can use that for IPC. You have a couple of options here:

  • Unix domains sockets

  • A non-XPC Mach service, such as CFMessagePort

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I found that CFMessagePort is working fine for the appex but not for the sysex (where the network extension is listening on a port, and the menu login-item app is connecting to it to get status updates). Is this expected or should it be working?

The exact same code works fine for our App Store build using an appex.

In generally I find it that IPC with network extensions (appex vs sysex) requires completely different approaches, which is a huge time sink for development (and I'd love to avoid!).

Thanks for all the help!

I found that CFMessagePort

Blergh, CFMessagePort.

is working fine for the appex but not for > the sysex … Is this expected or should it be working?

I think that you should be able to make this work. CFMessagePort has, for compatibility reasons, some interesting semantics where the listener (CFMessagePortCreateLocal) will try to check in with launchd and, if that fails, dynamically register the name with the bootstrap service. So, you have to make sure that the name you supply is known to launchd and that the check in is allowed by the sandbox. Specifically:

  • It must match your NEMachServiceName property.

  • Its value must be an immediately ‘child’ of an app group you’re entitled to use. For example, if you have an app group SKMME9E2Y8.com.example.myapp.shared, the name might be something like SKMME9E2Y8.com.example.myapp.shared.ipc.

App groups have some sharp edges on macOS. For the details, see App Groups: macOS vs iOS: Fight!.

In generally I find it that IPC with network extensions … is a huge time sink for development

Agreed. I get the impression that the current design is more an accident of history than any specific plan to mess you around (-: See my comments here.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

The strange thing is CFMessagePortCreateLocal is succeeding, CFMessagePortCreateRemote is also succeeding (which I imagine means the port lookup is working well on both ends), but calls to CFMessagePortSendRequest are timing out only for the sysex.

I'm testing different things following your indications, but I was wondering if this scenario could be indicative of a specific problem to look at?

Is the client also sandboxed? If so, it’ll need to also be entitled to use that app group.

Are you seeing any sandbox violations? See Discovering and diagnosing App Sandbox violations.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"