System Network Extension and Sleep

I've implemented a custom VPN for macOS (system extension, Packet Tunnel Provider, Developer ID). My tunneling logic uses BSD sockets. My VPN is configured with on-demand and should always connect when there's traffic:

targetManager?.isOnDemandEnabled = true
targetManager?.onDemandRules = [NEOnDemandRuleConnect()]

I have encountered some issues when the device enters sleep (or waking up from sleep). I've tried two scenarios.

Scenario 1:

protocolConfiguration?.disconnectOnSleep = true

With this flag set, the OS will disconnect the VPN just before entering to sleep. However, there were cases when the OS disconnected the VPN but immediately restarted it - probably because of how I defined the on-demand rules. This resulted in the VPN disconnection, then trying to reconnect, and then the Mac entered sleep. When the Mac woke up, the VPN didn't work well.

  1. Is there a way to avoid waking up, just before the Mac enters sleep?

Scenario 2:

protocolConfiguration?.disconnectOnSleep = false Disconnect on sleep is unset, and I've implemented the sleep/wake functions at the provider. With this configuration, the OS won't disconnect the VPN, so even in sleep, the extension should stay 'alive,' so it won't have the problem from (1). But in this case, I had other problems:

  1. On sleep, I'm disconnecting the tunnel. But sometimes, on wake(), all my network calls fail. Are the interfaces still down? How can I detect this case from the system extension?

  2. Is it possible that the OS would call sleep and then quickly call wake?

  3. Is it possible that after sleep, the OS would call the startTunnelWithOptions() function?

  4. Is it possible to restart the extension from a clean state right from the wake() function?

Answered by DTS Engineer in 816946022

I don’t have any good answers for you for scenario 1.

Regarding scenario 2:

On sleep, I'm disconnecting the tunnel. But sometimes, on wake(), all my network calls fail. Are the interfaces still down?

Probably. Your wake code is racing with all the other wake code on the system, including the code that’s bringing up the network interfaces.

My generally recommendation here is that you use a modern networking API, like Network framework, that supports waiting for connectivity. In that case I’d expect to see your NWConnection enter the .waiting(…) state and then, once the necessary infrastructure is up, connect and then enter the .ready state.

So:

  • Are you using NWConnection?

  • If so, it is working as I described above? If not, how is it failing?

  • If if you’re not using NWConnection, can you try it? I’m not suggesting that you rewrite all the networking code (well, not yet :-) but instead just try starting a parallel NWConnection and see how it behaves.

Is it possible that the OS would call sleep() and then quickly call wake()?

Yes. In sleep/wake scenarios pretty much anything is possible (-:

Is it possible that after sleep(), the OS would call the startTunnelWithOptions() function?

I wouldn’t expect to see that. NE should only start your tunnel if it’s stopped, and it doesn’t know that your tunnel is stopped until you tell it that.

Is it possible to restart the extension from a clean state right from the wake() function?

Not really.

Well, the closest thing to this is to set disconnectOnSleep, but that brings you back to scenario 1.

Share and Enjoy

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

Accepted Answer

I don’t have any good answers for you for scenario 1.

Regarding scenario 2:

On sleep, I'm disconnecting the tunnel. But sometimes, on wake(), all my network calls fail. Are the interfaces still down?

Probably. Your wake code is racing with all the other wake code on the system, including the code that’s bringing up the network interfaces.

My generally recommendation here is that you use a modern networking API, like Network framework, that supports waiting for connectivity. In that case I’d expect to see your NWConnection enter the .waiting(…) state and then, once the necessary infrastructure is up, connect and then enter the .ready state.

So:

  • Are you using NWConnection?

  • If so, it is working as I described above? If not, how is it failing?

  • If if you’re not using NWConnection, can you try it? I’m not suggesting that you rewrite all the networking code (well, not yet :-) but instead just try starting a parallel NWConnection and see how it behaves.

Is it possible that the OS would call sleep() and then quickly call wake()?

Yes. In sleep/wake scenarios pretty much anything is possible (-:

Is it possible that after sleep(), the OS would call the startTunnelWithOptions() function?

I wouldn’t expect to see that. NE should only start your tunnel if it’s stopped, and it doesn’t know that your tunnel is stopped until you tell it that.

Is it possible to restart the extension from a clean state right from the wake() function?

Not really.

Well, the closest thing to this is to set disconnectOnSleep, but that brings you back to scenario 1.

Share and Enjoy

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

Thanks eskimo!

if you’re not using NWConnection, can you try it?

Sure. I'm not working with NWConnection, and I'll try it soon.

A related question to the above: I'll add the code to the wake() function.

  1. I assume I should wait for '.ready' and only then to start establishing the tunnel. But what if the check will stuck in '.waiting'?
  2. Should I, at any point in the wake() function, set the 'reasserting' variable?
I assume I should wait for .ready and only then to start establishing the tunnel.

That depends on what you mean by “tunnel”. From iOS’s perspective, the tunnel is ‘up’ until you explicitly stop it. From your perspective, the tunnel is represented by the network connection. It can be down while the tunnel is up.

But what if the check will stuck in .waiting?

That’s the same as if you were establishing the tunnel in the first place. You can choose to wait forever or apply your own timeout.

Should I, at any point in the wake() function, set the reasserting variable?

Yes. See here.

Share and Enjoy

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

If if you’re not using NWConnection, can you try it? I’m not suggesting that you rewrite all the networking code (well, not yet :-) but instead just try starting a parallel NWConnection and see how it behaves.

For that, I'll have to add 'import Network' at the Packet Tunnel Provider. Then, my code to monitor path-change won't compile:

let oldPathOpt = change?[.oldKey] as? NWPath //'NWPath' is ambiguous for type lookup in this context

I also tried this one

let oldPathOpt = change?[.oldKey] as? NetworkExtension.NWPath //Ambiguous type name 'NWPath' in module 'NetworkExtension'

So, instead of NWConnection, would it be okay to use something like let configuration = URLSessionConfiguration.ephemeral configuration.waitsForConnectivity = true let session = URLSession(configuration: configuration) ?

System Network Extension and Sleep
 
 
Q